完成插件系统的基本设计,接着需要优化一些内容,标记了 TODO

This commit is contained in:
2026-02-07 05:44:57 +08:00
parent 1d9931f79d
commit 13512df9ce
7 changed files with 585 additions and 472 deletions

View File

@@ -93,24 +93,76 @@ from .data import SingleMusic, SingleTrack
# ]
# ========================
# 枚举类
# ========================
class PluginTypes(str, Enum):
"""插件类型枚举"""
FUNCTION_MUSIC_IMPORT = "import_music_data"
FUNCTION_TRACK_IMPORT = "import_track_data"
FUNCTION_MUSIC_OPERATE = "music_data_operating"
FUNCTION_TRACK_OPERATE = "track_data_operating"
FUNCTION_MUSIC_EXPORT = "export_music_data"
FUNCTION_TRACK_EXPORT = "export_track_data"
SERVICE = "service"
LIBRARY = "library"
# ========================
# 数据类
# ========================
@dataclass
class PluginConfig(ABC):
"""插件配置基类"""
def to_dict(self) -> Dict[str, Any]:
"""字典化配置文件"""
"""将配置内容转换为字典
返回
====
Dict[str, Any]
配置项的字典表示,不包含以下划线开头的私有属性
"""
return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "PluginConfig":
"""从字典创建配置实例"""
"""从字典创建配置实例
参数
====
data: Dict[str, Any]
包含配置字段的字典
返回
====
PluginConfig
配置类的实例
"""
# 只保留类中定义的字段
field_names = {f.name for f in cls.__dataclass_fields__.values()}
filtered_data = {k: v for k, v in data.items() if k in field_names}
return cls(**filtered_data)
def save_to_file(self, file_path: Path) -> None:
"""保存配置到文件"""
"""保存配置到 TOML 文件
参数
====
file_path: Path
目标文件路径;必须以 .toml 为后缀
异常
====
PluginConfigDumpError
当文件后缀不是 .toml 或写入失败时抛出
"""
if file_path.suffix.upper() == ".TOML":
file_path.parent.mkdir(parents=True, exist_ok=True)
else:
@@ -126,7 +178,23 @@ class PluginConfig(ABC):
@classmethod
def load_from_file(cls, file_path: Path) -> "PluginConfig":
"""从文件加载配置"""
""" TOML 文件加载配置
参数
====
file_path: Path
源文件路径
返回
====
PluginConfig
加载后的配置实例
异常
====
PluginConfigLoadError
当读取或解析失败时抛出
"""
try:
with file_path.open("rb") as f:
return cls.from_dict(tomllib.load(f))
@@ -134,19 +202,6 @@ class PluginConfig(ABC):
raise PluginConfigLoadError(e)
class PluginType(str, Enum):
"""插件类型枚举"""
FUNCTION_MUSIC_IMPORT = "import_music_data"
FUNCTION_TRACK_IMPORT = "import_track_data"
FUNCTION_MUSIC_OPERATE = "music_data_operating"
FUNCTION_TRACK_OPERATE = "track_data_operating"
FUNCTION_MUSIC_EXPORT = "export_music_data"
FUNCTION_TRACK_EXPORT = "export_track_data"
SERVICE = "service"
LIBRARY = "library"
@dataclass
class PluginMetaInformation(ABC):
"""插件元信息"""
@@ -159,7 +214,7 @@ class PluginMetaInformation(ABC):
"""插件简介"""
version: Tuple[int, ...]
"""插件版本号"""
type: PluginType
type: PluginTypes
"""插件类型"""
license: str = "MIT License"
"""插件发布时采用的许可协议"""
@@ -167,6 +222,11 @@ class PluginMetaInformation(ABC):
"""插件是否对其他插件存在依赖"""
# ========================
# 抽象基类
# ========================
class TopPluginBase(ABC):
"""所有插件的抽象基类"""
@@ -195,7 +255,7 @@ class TopInOutPluginBase(TopPluginBase, ABC):
"""导入导出用抽象基类"""
supported_formats: Tuple[str, ...] = tuple()
"""支持的格式"""
"""支持的格式(定义后会自动转大写)"""
def __init_subclass__(cls) -> None:
super().__init_subclass__()
@@ -214,11 +274,33 @@ class TopInOutPluginBase(TopPluginBase, ABC):
)
def can_handle_file(self, file_path: Path) -> bool:
"""判断是否可处理某个文件"""
"""判断是否可处理某个文件
参数
====
file_path: Path
待检测的文件路径
返回
====
bool
若文件后缀已在本类中定义,则返回 True
"""
return file_path.suffix.upper().endswith(self.supported_formats)
def can_handle_format(self, format_name: str) -> bool:
"""判断是否可处理某个格式"""
"""判断是否可处理某个格式
参数
====
format_name: str
格式名称(如 'MIDI', 'WAV'
返回
====
bool
若格式名本类中已经定义,则返回 True
"""
return format_name.upper().endswith(self.supported_formats)
@@ -228,7 +310,7 @@ class MusicInputPluginBase(TopInOutPluginBase, ABC):
def __init_subclass__(cls) -> None:
super().__init_subclass__()
if cls.metainfo.type != PluginType.FUNCTION_MUSIC_IMPORT:
if cls.metainfo.type != PluginTypes.FUNCTION_MUSIC_IMPORT:
raise PluginMetainfoValueError(
"插件类`{cls_name}`是从`MusicInputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_MUSIC_IMPORT`类型的插件,而不是`PluginType.{cls_type}`".format(
cls_name=cls.__name__,
@@ -240,11 +322,38 @@ class MusicInputPluginBase(TopInOutPluginBase, ABC):
def loadbytes(
self, bytes_buffer_in: BinaryIO, config: Optional[PluginConfig]
) -> "SingleMusic":
"""从字节流加载数据到完整曲目"""
"""从字节流加载数据到完整曲目
参数
====
bytes_buffer_in: BinaryIO
输入的二进制字节流
config: Optional[PluginConfig]
插件配置;**可选**
返回
====
SingleMusic
解析得到的完整曲目对象
"""
pass
def load(self, file_path: Path, config: Optional[PluginConfig]) -> "SingleMusic":
"""从文件加载数据到完整曲目"""
"""从文件加载数据到完整曲目
参数
====
file_path: Path
输入文件路径
config: Optional[PluginConfig]
插件配置;**可选**
返回
====
SingleMusic
解析得到的完整曲目对象
"""
with file_path.open("rb") as f:
return self.loadbytes(f, config)
@@ -255,7 +364,7 @@ class TrackInputPluginBase(TopInOutPluginBase, ABC):
def __init_subclass__(cls) -> None:
super().__init_subclass__()
if cls.metainfo.type != PluginType.FUNCTION_TRACK_IMPORT:
if cls.metainfo.type != PluginTypes.FUNCTION_TRACK_IMPORT:
raise PluginMetainfoValueError(
"插件类`{cls_name}`是从`TrackInputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_TRACK_IMPORT`类型的插件,而不是`PluginType.{cls_type}`".format(
cls_name=cls.__name__,
@@ -267,11 +376,37 @@ class TrackInputPluginBase(TopInOutPluginBase, ABC):
def loadbytes(
self, bytes_buffer_in: BinaryIO, config: Optional[PluginConfig]
) -> "SingleTrack":
"""从字节流加载音符数据到单个音轨"""
"""从字节流加载音符数据到单个音轨
参数
====
bytes_buffer_in: BinaryIO
输入的二进制字节流
config: Optional[PluginConfig]
插件配置;**可选**
返回
====
SingleTrack
解析得到的单个音轨对象
"""
pass
def load(self, file_path: Path, config: Optional[PluginConfig]) -> "SingleTrack":
"""从文件加载音符数据到单个音轨"""
"""从文件加载音符数据到单个音轨
参数
====
file_path: Path
输入文件路径
config: Optional[PluginConfig]
插件配置;**可选**
返回
====
SingleTrack
解析得到的单个音轨对象
"""
with file_path.open("rb") as f:
return self.loadbytes(f, config)
@@ -282,7 +417,7 @@ class MusicOperatePluginBase(TopPluginBase, ABC):
def __init_subclass__(cls) -> None:
super().__init_subclass__()
if cls.metainfo.type != PluginType.FUNCTION_MUSIC_OPERATE:
if cls.metainfo.type != PluginTypes.FUNCTION_MUSIC_OPERATE:
raise PluginMetainfoValueError(
"插件类`{cls_name}`是从`MusicOperatePlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_MUSIC_OPERATE`类型的插件,而不是`PluginType.{cls_type}`".format(
cls_name=cls.__name__,
@@ -294,7 +429,20 @@ class MusicOperatePluginBase(TopPluginBase, ABC):
def process(
self, data: "SingleMusic", config: Optional[PluginConfig]
) -> "SingleMusic":
"""处理完整曲目的数据"""
"""处理完整曲目的数据
参数
====
data: SingleMusic
待处理的完整曲目
config: Optional[PluginConfig]
插件配置;**可选**
返回
====
SingleMusic
处理后的完整曲目
"""
pass
@@ -304,7 +452,7 @@ class TrackOperatePluginBase(TopPluginBase, ABC):
def __init_subclass__(cls) -> None:
super().__init_subclass__()
if cls.metainfo.type != PluginType.FUNCTION_TRACK_OPERATE:
if cls.metainfo.type != PluginTypes.FUNCTION_TRACK_OPERATE:
raise PluginMetainfoValueError(
"插件类`{cls_name}`是从`TrackOperatePlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_TRACK_OPERATE`类型的插件,而不是`PluginType.{cls_type}`".format(
cls_name=cls.__name__,
@@ -316,7 +464,20 @@ class TrackOperatePluginBase(TopPluginBase, ABC):
def process(
self, data: "SingleTrack", config: Optional[PluginConfig]
) -> "SingleTrack":
"""处理单个音轨的音符数据"""
"""处理单个音轨的音符数据
参数
====
data: SingleTrack
待处理的单个音轨
config: Optional[PluginConfig]
插件配置;**可选**
返回
====
SingleTrack
处理后的单个音轨
"""
pass
@@ -326,7 +487,7 @@ class MusicOutputPluginBase(TopInOutPluginBase, ABC):
def __init_subclass__(cls) -> None:
super().__init_subclass__()
if cls.metainfo.type != PluginType.FUNCTION_MUSIC_EXPORT:
if cls.metainfo.type != PluginTypes.FUNCTION_MUSIC_EXPORT:
raise PluginMetainfoValueError(
"插件类`{cls_name}`是从`MusicOutputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_MUSIC_EXPORT`类型的插件,而不是`PluginType.{cls_type}`".format(
cls_name=cls.__name__,
@@ -338,14 +499,38 @@ class MusicOutputPluginBase(TopInOutPluginBase, ABC):
def dumpbytes(
self, data: "SingleMusic", config: Optional[PluginConfig]
) -> BinaryIO:
"""将完整曲目导出为对应格式的字节流"""
"""将完整曲目导出为对应格式的字节流
参数
====
data: SingleMusic
待导出的完整曲目
config: Optional[PluginConfig]
插件配置;**可选**
返回
====
BinaryIO
导出后的二进制字节流
"""
pass
@abstractmethod
def dump(
self, data: "SingleMusic", file_path: Path, config: Optional[PluginConfig]
):
"""将完整曲目导出为对应格式的文件"""
"""将完整曲目导出为对应格式的文件
参数
====
data: SingleMusic
待导出的完整曲目
file_path: Path
输出文件路径
config: Optional[PluginConfig]
插件配置;**可选**
"""
pass
@@ -355,7 +540,7 @@ class TrackOutputPluginBase(TopInOutPluginBase, ABC):
def __init_subclass__(cls) -> None:
super().__init_subclass__()
if cls.metainfo.type != PluginType.FUNCTION_TRACK_EXPORT:
if cls.metainfo.type != PluginTypes.FUNCTION_TRACK_EXPORT:
raise PluginMetainfoValueError(
"插件类`{cls_name}`是从`TrackOutputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_TRACK_EXPORT`类型的插件,而不是`PluginType.{cls_type}`".format(
cls_name=cls.__name__,
@@ -367,14 +552,37 @@ class TrackOutputPluginBase(TopInOutPluginBase, ABC):
def dumpbytes(
self, data: "SingleTrack", config: Optional[PluginConfig]
) -> BinaryIO:
"""将单个音轨导出为对应格式的字节流"""
"""将单个音轨导出为对应格式的字节流
参数
====
data: SingleTrack
待导出的单个音轨
config: Optional[PluginConfig]
插件配置;**可选**
返回
====
BinaryIO
导出后的二进制字节流
"""
pass
@abstractmethod
def dump(
self, data: "SingleTrack", file_path: Path, config: Optional[PluginConfig]
):
"""将单个音轨导出为对应格式的文件"""
"""将单个音轨导出为对应格式的文件
参数
====
data: SingleTrack
待导出的单个音轨
file_path: Path
输出文件路径
config: Optional[PluginConfig]
插件配置;**可选**
"""
pass
@@ -384,7 +592,7 @@ class ServicePluginBase(TopPluginBase, ABC):
def __init_subclass__(cls) -> None:
super().__init_subclass__()
if cls.metainfo.type != PluginType.SERVICE:
if cls.metainfo.type != PluginTypes.SERVICE:
raise PluginMetainfoValueError(
"插件类`{cls_name}`是从`ServicePlugin`继承的,该类的子类应当为一个`PluginType.SERVICE`类型的插件,而不是`PluginType.{cls_type}`".format(
cls_name=cls.__name__,
@@ -394,7 +602,15 @@ class ServicePluginBase(TopPluginBase, ABC):
@abstractmethod
def serve(self, config: Optional[PluginConfig], *args) -> None:
"""服务插件的运行逻辑"""
"""服务插件的运行逻辑
参数
====
config: Optional[PluginConfig]
插件配置;**可选**
*args: Any
其他运行时参数
"""
pass
@@ -404,7 +620,7 @@ class LibraryPluginBase(TopPluginBase, ABC):
def __init_subclass__(cls) -> None:
super().__init_subclass__()
if cls.metainfo.type != PluginType.LIBRARY:
if cls.metainfo.type != PluginTypes.LIBRARY:
raise PluginMetainfoValueError(
"插件类`{cls_name}`是从`LibraryPlugin`继承的,该类的子类应当为一个`PluginType.LIBRARY`类型的插件,而不是`PluginType.{cls_type}`".format(
cls_name=cls.__name__,

View File

@@ -27,7 +27,7 @@ from Musicreater.plugins import (
music_input_plugin,
PluginConfig,
PluginMetaInformation,
PluginType,
PluginTypes,
MusicInputPluginBase,
)
@@ -37,11 +37,11 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
"""Midi 音乐数据导入插件"""
metainfo = PluginMetaInformation(
name="midi_2_music_plugin",
name="Midi 导入插件",
author="金羿、玉衡Alioth",
description="从 Midi 文件导入音乐数据",
version=(0, 0, 1),
type=PluginType.FUNCTION_MUSIC_IMPORT,
type=PluginTypes.FUNCTION_MUSIC_IMPORT,
license="Same as Musicreater",
)

View File

@@ -476,14 +476,7 @@ class SingleTrack(List[SingleNote]):
self.extra_info = extra_information if extra_information else {}
self.argument_curves = {
CurvableParam.PITCH: None,
CurvableParam.VELOCITY: None,
CurvableParam.VOLUME: None,
CurvableParam.DISTANCE: None,
CurvableParam.LR_PANNING: None,
CurvableParam.UD_PANNING: None,
}
self.argument_curves = {item: None for item in CurvableParam}
super().__init__(*args)
super().sort()
@@ -572,9 +565,9 @@ class SingleTrack(List[SingleNote]):
is_percussive_note=self.is_percussive,
sound_position=self.sound_position,
**{
item.value: self.argument_curves[item].value_at(_note.start_time) # type: ignore
item.value: argcrv.value_at(_note.start_time)
for item in CurvableParam
if self.argument_curves[item]
if (argcrv := self.argument_curves[item])
},
)

View File

@@ -225,6 +225,23 @@ class PluginLoadError(MusicreaterOuterlyError):
super().__init__("插件加载错误 - ", *args)
class PluginNotFoundError(PluginLoadError):
"""插件未找到"""
def __init__(self, *args):
"""插件未找到"""
super().__init__("插件未找到:", *args)
class PluginRegisteredError(PluginLoadError):
"""插件重复注册"""
def __init__(self, *args):
"""插件已被注册注册"""
super().__init__("插件重复注册:", *args)
class PluginConfigRelatedError(MusicreaterOuterlyError):
"""插件配置相关错误"""

View File

@@ -47,19 +47,25 @@ import re
from difflib import get_close_matches
from typing import Dict, Generator, List, Optional, Tuple, Union
from typing import Dict, Generator, List, Optional, Tuple, Union, Mapping, Callable
from pathlib import Path
from .data import SingleMusic, SingleTrack
from .exceptions import FileFormatNotSupportedError, PluginNotSpecifiedError
from .exceptions import (
FileFormatNotSupportedError,
PluginNotSpecifiedError,
PluginNotFoundError,
)
from ._plugin_abc import TopPluginBase
from .plugins import (
_global_plugin_registry,
PluginRegistry,
PluginConfig,
PluginType,
PluginTypes,
load_plugin_module,
T_IOPlugin,
T_Plugin,
)
@@ -72,7 +78,7 @@ class MusiCreater:
__plugin_registry: PluginRegistry
"""插件注册表实例"""
_plugin_cache: Dict[str, TopPluginBase]
"""插件缓存字典,插件为键、插件实例为值"""
"""插件缓存字典,插件id为键、插件实例为值"""
music: SingleMusic
"""当前曲目实例"""
@@ -86,149 +92,119 @@ class MusiCreater:
self.music = whole_music
@staticmethod
def _get_plugin_within_iousage(
get_func: Callable[[Union[Path, str]], Generator[T_IOPlugin, None, None]],
fpath: Path,
plg_regdict: Dict[str, T_IOPlugin],
plg_id: Optional[str],
) -> T_IOPlugin:
__plugin: Optional[T_IOPlugin] = None
if plg_id:
__plugin = plg_regdict.get(plg_id)
else:
for __plg in get_func(fpath):
if __plugin:
raise PluginNotSpecifiedError(
"文件类型`{}`可被多个插件处理,请在调用函数的参数中指定插件名称".format(
fpath.suffix.upper()
)
)
__plugin = __plg
if __plugin:
return __plugin
else:
raise FileFormatNotSupportedError(
"无法找到处理`{}`类型文件的插件".format(fpath.suffix.upper())
)
@classmethod
def import_music(
cls,
file_path: Path,
plugin_name: Optional[str] = None,
plugin_id: Optional[str] = None,
plugin_config: Optional[PluginConfig] = None,
):
__music = None
if plugin_name:
__music = _global_plugin_registry.get_music_input_plugin(plugin_name).load(
file_path, plugin_config
)
else:
for plugin in _global_plugin_registry.get_music_input_plugin_by_format(
file_path
):
if __music is not None:
raise PluginNotSpecifiedError(
"文件类型`{}`可被多个插件处理,请在导入函数的参数中指定插件名称".format(
file_path.suffix.upper()
)
)
__music = plugin.load(file_path, plugin_config)
if __music is None:
raise FileFormatNotSupportedError(
"无法找到处理`{}`类型文件的插件".format(file_path.suffix.upper())
)
return cls(whole_music=__music)
) -> "MusiCreater":
return cls(
whole_music=cls._get_plugin_within_iousage(
_global_plugin_registry.get_music_input_plugin_by_format,
file_path,
_global_plugin_registry._music_input_plugins,
plugin_id,
).load(file_path, plugin_config)
)
def import_track(
self,
file_path: Path,
plugin_name: Optional[str] = None,
plugin_id: Optional[str] = None,
plugin_config: Optional[PluginConfig] = None,
) -> SingleTrack:
__track = None
if plugin_name:
__track = self.get_plugin_by_name(
plugin_name
).load( # pyright: ignore[reportAttributeAccessIssue]
file_path, plugin_config
)
else:
for plugin in self.__plugin_registry.get_track_input_plugin_by_format(
file_path
):
if __track:
raise PluginNotSpecifiedError(
"文件类型`{}`可被多个插件处理,请在导入函数的参数中指定插件名称".format(
file_path.suffix.upper()
)
)
__track = plugin.load(file_path, plugin_config)
if __track:
self.music.append(__track)
return __track
raise FileFormatNotSupportedError(
"无法找到处理`{}`类型文件的插件".format(file_path.suffix.upper())
self.music.append(
self._get_plugin_within_iousage(
self.__plugin_registry.get_track_input_plugin_by_format,
file_path,
self.__plugin_registry._track_input_plugins,
plugin_id,
).load(file_path, plugin_config)
)
return self.music[-1]
def export_music(
self,
file_path: Path,
plugin_name: Optional[str] = None,
plugin_id: Optional[str] = None,
plugin_config: Optional[PluginConfig] = None,
) -> None:
__plugin = None
if plugin_name:
__plugin = self.get_plugin_by_name(plugin_name)
else:
for plugin in self.__plugin_registry.get_music_output_plugin_by_format(
file_path
):
if __plugin:
raise PluginNotSpecifiedError(
"文件类型`{}`可被多个插件处理,请在导出函数的参数中指定插件名称".format(
file_path.suffix.upper()
)
)
__plugin = plugin
if __plugin:
__plugin.dump( # pyright: ignore[reportAttributeAccessIssue]
self.music, file_path, plugin_config
)
else:
raise FileFormatNotSupportedError(
"无法找到处理`{}`类型文件的插件".format(file_path.suffix.upper())
)
self._get_plugin_within_iousage(
self.__plugin_registry.get_music_output_plugin_by_format,
file_path,
self.__plugin_registry._music_output_plugins,
plugin_id,
).dump(self.music, file_path, plugin_config)
def export_track(
self,
track_index: int,
file_path: Path,
plugin_name: Optional[str] = None,
plugin_id: Optional[str] = None,
plugin_config: Optional[PluginConfig] = None,
) -> None:
__plugin = None
if plugin_name:
__plugin = self.get_plugin_by_name(plugin_name)
else:
for plugin in self.__plugin_registry.get_track_output_plugin_by_format(
file_path
):
if __plugin:
raise PluginNotSpecifiedError(
"文件类型`{}`可被多个插件处理,请在导出函数的参数中指定插件名称".format(
file_path.suffix.upper()
)
)
__plugin = plugin
if __plugin:
__plugin.dump( # pyright: ignore[reportAttributeAccessIssue]
self.music[track_index], file_path, plugin_config
)
else:
raise FileFormatNotSupportedError(
"无法找到处理`{}`类型文件的插件".format(file_path.suffix.upper())
)
self._get_plugin_within_iousage(
self.__plugin_registry.get_track_output_plugin_by_format,
file_path,
self.__plugin_registry._track_output_plugins,
plugin_id,
).dump(self.music[track_index], file_path, plugin_config)
def perform_operation_on_music(
self, plugin_name: str, plugin_config: Optional[PluginConfig] = None
self, plugin_id: str, plugin_config: Optional[PluginConfig] = None
):
# 这样做是为了兼容以后的*撤回/重做*功能
self.music = self.get_plugin_by_name(
plugin_name
).process( # pyright: ignore[reportAttributeAccessIssue]
self.music, plugin_config
)
if __plugin := self.__plugin_registry._music_operate_plugins.get(plugin_id):
# 这样做是为了兼容以后的*撤回/重做*功能
self.music = __plugin.process(self.music, plugin_config)
else:
raise PluginNotFoundError(
"无法找到惟一识别码为`{}`的插件".format(plugin_id)
)
def perform_operation_on_track(
self,
track_index: int,
plugin_name: str,
plugin_id: str,
plugin_config: Optional[PluginConfig] = None,
):
# 这样做是为了兼容以后的*撤回/重做*功能
self.music[track_index] = self.get_plugin_by_name(
plugin_name
).process( # pyright: ignore[reportAttributeAccessIssue]
self.music[track_index], plugin_config
)
if __plugin := self.__plugin_registry._track_operate_plugins.get(plugin_id):
# 这样做是为了兼容以后的*撤回/重做*功能
self.music[track_index] = __plugin.process(
self.music[track_index], plugin_config
)
else:
raise PluginNotFoundError(
"无法找到惟一识别码为`{}`的插件".format(plugin_id)
)
@staticmethod
def _camel_to_snake(name: str) -> str:
@@ -240,8 +216,8 @@ class MusiCreater:
"([a-z0-9])([A-Z])", r"\1_\2", re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
).lower()
def _parse_plugin_name(self, attr_name: str) -> Optional[str]:
"""解析属性名称为插件名称"""
def _parse_plugin_id(self, attr_name: str) -> Optional[str]:
"""解析属性名称为插件惟一识别码"""
# 尝试去除 _plugin 后缀
if attr_name.endswith("_plugin"):
@@ -256,35 +232,35 @@ class MusiCreater:
if snake_case_name in self._plugin_cache: # 尝试转换后的插件名
return snake_case_name
else:
return self._parse_plugin_name(snake_case_name)
return self._parse_plugin_id(snake_case_name)
return None
def _get_closest_plugin_name(self, requested_name: str) -> Optional[str]:
"""找到最接近的插件名称(用于更好的错误提示)"""
def _get_closest_plugin_id(self, requested_id: str) -> Optional[str]:
"""找到最接近的插件识别码(用于更好的错误提示)"""
matches = get_close_matches(
requested_name, self._plugin_cache.keys(), n=1, cutoff=0.6
requested_id, self._plugin_cache.keys(), n=1, cutoff=0.6
)
return matches[0] if matches else None
def get_plugin_by_name(self, name: str) -> TopPluginBase:
def get_plugin_by_id(self, plg_id: str):
"""获取插件实例,并缓存起来,提高性能"""
if name.startswith("_"):
raise AttributeError("属性`{}`不存在,不应访问类的私有属性".format(name))
if plg_id.startswith("_"):
raise AttributeError("属性`{}`不存在,不应访问类的私有属性".format(plg_id))
if name in self._plugin_cache:
return self._plugin_cache[name]
if plg_id in self._plugin_cache:
return self._plugin_cache[plg_id]
else:
plugin_name = self._parse_plugin_name(name)
plugin_name = self._parse_plugin_id(plg_id)
if plugin_name:
self._plugin_cache[name] = self._plugin_cache[plugin_name]
return self._plugin_cache[name]
self._plugin_cache[plg_id] = self._plugin_cache[plugin_name]
return self._plugin_cache[plg_id]
closest = self._get_closest_plugin_name(name)
closest = self._get_closest_plugin_id(plg_id)
raise AttributeError(
"插件`{}`不存在,请检查插件名称是否正确".format(name)
"插件`{}`不存在,请检查插件的惟一识别码是否正确".format(plg_id)
+ (
";或者阁下可能想要使用的是`{}`插件?".format(closest)
if closest
@@ -292,18 +268,18 @@ class MusiCreater:
)
)
def __getattr__(self, name: str):
def __getattr__(self, plugin_id: str):
"""动态属性访问,允许直接 实例.插件名 来访问插件"""
return self.get_plugin_by_name(name)
return self.get_plugin_by_id(plugin_id)
def _cache_all_plugins(self):
"""获取所有已注册插件的名称"""
for __plugin_type, __plugins_set in self.__plugin_registry:
for __plugin in __plugins_set:
if __plugin.metainfo.name in self._plugin_cache: # 避免重复缓存
for __plugin_type, __plugins_map in self.__plugin_registry:
for __plugin_id, __plugin in __plugins_map.items():
if __plugin_id in self._plugin_cache: # 避免重复缓存
if (
__plugin.metainfo.version
<= self._plugin_cache[__plugin.metainfo.name].metainfo.version
<= self._plugin_cache[__plugin_id].metainfo.version
): # 优先使用版本号最大的插件
continue
self._plugin_cache[__plugin.metainfo.name] = __plugin
self._plugin_cache[__plugin_id] = __plugin

View File

@@ -30,13 +30,16 @@ from typing import (
Set,
Iterable,
Iterator,
TypeVar,
Mapping,
Callable,
)
from itertools import chain
from ._plugin_abc import (
# 枚举类
PluginType,
PluginTypes,
# 抽象基类/数据类(插件参数定义)
PluginConfig,
PluginMetaInformation,
@@ -56,12 +59,13 @@ from .exceptions import (
PluginMetainfoNotFoundError,
ParameterTypeError,
PluginInstanceNotFoundError,
PluginRegisteredError,
)
__all__ = [
# 枚举类
"PluginType",
"PluginTypes",
# 抽象基类/数据类(插件参数定义)
"PluginConfig",
"PluginMetaInformation",
@@ -86,6 +90,26 @@ __all__ = [
]
T_IOPlugin = TypeVar(
"T_IOPlugin",
MusicInputPluginBase,
TrackInputPluginBase,
MusicOutputPluginBase,
TrackOutputPluginBase,
)
T_Plugin = TypeVar(
"T_Plugin",
MusicInputPluginBase,
TrackInputPluginBase,
MusicOperatePluginBase,
TrackOperatePluginBase,
MusicOutputPluginBase,
TrackOutputPluginBase,
ServicePluginBase,
LibraryPluginBase,
)
def load_plugin_module(package: Union[Path, str]):
"""自动发现并加载插件包中的插件
@@ -111,290 +135,199 @@ class PluginRegistry:
"""插件注册管理器(注册表)"""
def __init__(self):
# 实际上在纵容那些有着同样名称的插件……
# (不用 Dict[str`plugin name`, PluginClass`] 的形式)
# 啊,我真的很高尚
# 你真的不会把插件注册两遍吧……对吧?
self._music_input_plugins: Dict[str, MusicInputPluginBase] = {}
self._track_input_plugins: Dict[str, TrackInputPluginBase] = {}
self._music_operate_plugins: Dict[str, MusicOperatePluginBase] = {}
self._track_operate_plugins: Dict[str, TrackOperatePluginBase] = {}
self._music_output_plugins: Dict[str, MusicOutputPluginBase] = {}
self._track_output_plugins: Dict[str, TrackOutputPluginBase] = {}
self._service_plugins: Dict[str, ServicePluginBase] = {}
self._library_plugins: Dict[str, LibraryPluginBase] = {}
# EMERGENCY TODO ================================================ CRITICAL 紧急更改
# 改成 Dict[str`plugin id`, PluginClass`]] 的形式吧
# 刚刚才想起来,这个 name 是显示名称啊!草
# 现在测试情况下就将错就错吧,先把 name 当成 id 来写吧
self._music_input_plugins: Set[MusicInputPluginBase] = set()
self._track_input_plugins: Set[TrackInputPluginBase] = set()
self._music_operate_plugins: Set[MusicOperatePluginBase] = set()
self._track_operate_plugins: Set[TrackOperatePluginBase] = set()
self._music_output_plugins: Set[MusicOutputPluginBase] = set()
self._track_output_plugins: Set[TrackOutputPluginBase] = set()
self._service_plugins: Set[ServicePluginBase] = set()
self._library_plugins: Set[LibraryPluginBase] = set()
def __iter__(self) -> Iterator[Tuple[PluginType, Set[TopPluginBase]]]:
def __iter__(self) -> Iterator[Tuple[PluginTypes, Mapping[str, TopPluginBase]]]:
"""迭代器,返回所有插件"""
return iter(
(
(PluginType.FUNCTION_MUSIC_IMPORT, self._music_input_plugins),
(PluginType.FUNCTION_TRACK_IMPORT, self._track_input_plugins),
(PluginType.FUNCTION_MUSIC_OPERATE, self._music_operate_plugins),
(PluginType.FUNCTION_TRACK_OPERATE, self._track_operate_plugins),
(PluginType.FUNCTION_MUSIC_EXPORT, self._music_output_plugins),
(PluginType.FUNCTION_TRACK_EXPORT, self._track_output_plugins),
(PluginType.SERVICE, self._service_plugins),
(PluginType.LIBRARY, self._library_plugins),
(PluginTypes.FUNCTION_MUSIC_IMPORT, self._music_input_plugins),
(PluginTypes.FUNCTION_TRACK_IMPORT, self._track_input_plugins),
(PluginTypes.FUNCTION_MUSIC_OPERATE, self._music_operate_plugins),
(PluginTypes.FUNCTION_TRACK_OPERATE, self._track_operate_plugins),
(PluginTypes.FUNCTION_MUSIC_EXPORT, self._music_output_plugins),
(PluginTypes.FUNCTION_TRACK_EXPORT, self._track_output_plugins),
(PluginTypes.SERVICE, self._service_plugins),
(PluginTypes.LIBRARY, self._library_plugins),
)
) # pyright: ignore[reportReturnType]
)
def register_music_input_plugin(self, plugin_class: type) -> None:
@staticmethod
def _register_plugin(cls_dict: dict, plg_class: type, plg_id: str) -> None:
"""注册插件"""
if plg_id in cls_dict:
if cls_dict[plg_id].metainfo.version >= plg_class.metainfo.version:
raise PluginRegisteredError(
"插件惟一识别码`{}`所对应的插件已存在更高版本`{}`,请勿重复注册同一插件!".format(
plg_id, plg_class.metainfo
)
)
cls_dict[plg_id] = plg_class()
def register_music_input_plugin(
self,
plugin_class: type,
plugin_id: str,
) -> None:
"""注册输入插件-整首曲目"""
self._music_input_plugins.add(plugin_class())
self._register_plugin(self._music_input_plugins, plugin_class, plugin_id)
def register_track_input_plugin(self, plugin_class: type) -> None:
def register_track_input_plugin(self, plugin_class: type, plugin_id: str) -> None:
"""注册输入插件-单个音轨"""
self._track_input_plugins.add(plugin_class())
self._register_plugin(self._track_input_plugins, plugin_class, plugin_id)
def register_music_operate_plugin(self, plugin_class: type) -> None:
def register_music_operate_plugin(self, plugin_class: type, plugin_id: str) -> None:
"""注册曲目处理插件"""
self._music_operate_plugins.add(plugin_class())
self._register_plugin(self._music_operate_plugins, plugin_class, plugin_id)
def register_track_operate_plugin(self, plugin_class: type) -> None:
def register_track_operate_plugin(self, plugin_class: type, plugin_id: str) -> None:
"""注册音轨处理插件"""
self._track_operate_plugins.add(plugin_class())
self._register_plugin(self._track_operate_plugins, plugin_class, plugin_id)
def register_music_output_plugin(self, plugin_class: type) -> None:
def register_music_output_plugin(self, plugin_class: type, plugin_id: str) -> None:
"""注册输出插件-整首曲目"""
self._music_output_plugins.add(plugin_class())
self._register_plugin(self._music_output_plugins, plugin_class, plugin_id)
def register_track_output_plugin(self, plugin_class: type) -> None:
def register_track_output_plugin(self, plugin_class: type, plugin_id: str) -> None:
"""注册输出插件-单个音轨"""
self._track_output_plugins.add(plugin_class())
self._register_plugin(self._track_output_plugins, plugin_class, plugin_id)
def register_service_plugin(self, plugin_class: type) -> None:
def register_service_plugin(self, plugin_class: type, plugin_id: str) -> None:
"""注册服务插件"""
self._service_plugins.add(plugin_class())
self._register_plugin(self._service_plugins, plugin_class, plugin_id)
def register_library_plugin(self, plugin_class: type) -> None:
def register_library_plugin(self, plugin_class: type, plugin_id: str) -> None:
"""注册支持库插件"""
self._library_plugins.add(plugin_class())
self._register_plugin(self._library_plugins, plugin_class, plugin_id)
@staticmethod
def _get_io_plugin_by_format(
plugin_regdict: Dict[str, T_IOPlugin], fpath_or_format: Union[Path, str]
) -> Generator[T_IOPlugin, None, None]:
if isinstance(fpath_or_format, str):
return (
plugin
for plugin in plugin_regdict.values()
if plugin.can_handle_format(fpath_or_format)
)
elif isinstance(fpath_or_format, Path):
return (
plugin
for plugin in plugin_regdict.values()
if plugin.can_handle_file(fpath_or_format)
)
else:
raise ParameterTypeError(
"用于指定“导入全曲的数据之类型”的参数,其类型须为`Path`路径或字符串,而非`{}`类型的`{}`值".format(
type(fpath_or_format), fpath_or_format
)
)
def get_music_input_plugin_by_format(
self, filepath_or_format: Union[Path, str]
) -> Generator[MusicInputPluginBase, None, None]:
"""通过指定输入的文件或格式,以获取对应的全曲导入用插件"""
if isinstance(filepath_or_format, str):
return (
plugin
for plugin in self._music_input_plugins
if plugin.can_handle_format(filepath_or_format)
)
elif isinstance(filepath_or_format, Path):
return (
plugin
for plugin in self._music_input_plugins
if plugin.can_handle_file(filepath_or_format)
)
else:
raise ParameterTypeError(
"用于指定“导入全曲的数据之类型”的参数,其类型须为`Path`路径或字符串,而非`{}`类型的`{}`值".format(
type(filepath_or_format), filepath_or_format
)
)
return self._get_io_plugin_by_format(
self._music_input_plugins, filepath_or_format
)
def get_track_input_plugin_by_format(
self, filepath_or_format: Union[Path, str]
) -> Generator[TrackInputPluginBase, None, None]:
"""通过指定输入的文件或格式,以获取对应的单音轨导入用插件"""
if isinstance(filepath_or_format, str):
return (
plugin
for plugin in self._track_input_plugins
if plugin.can_handle_format(filepath_or_format)
)
elif isinstance(filepath_or_format, Path):
return (
plugin
for plugin in self._track_input_plugins
if plugin.can_handle_file(filepath_or_format)
)
else:
raise ParameterTypeError(
"用于指定“导入单个音轨的数据之类型”的参数,其类型须为`Path`路径或字符串,而非`{}`类型的`{}`值".format(
type(filepath_or_format), filepath_or_format
)
)
return self._get_io_plugin_by_format(
self._track_input_plugins, filepath_or_format
)
def get_music_output_plugin_by_format(
self, filepath_or_format: Union[Path, str]
) -> Generator[MusicOutputPluginBase, None, None]:
"""通过指定输出的文件或格式,以获取对应的导出全曲用插件"""
if isinstance(filepath_or_format, str):
return (
plugin
for plugin in self._music_output_plugins
if plugin.can_handle_format(filepath_or_format)
)
elif isinstance(filepath_or_format, Path):
return (
plugin
for plugin in self._music_output_plugins
if plugin.can_handle_file(filepath_or_format)
)
else:
raise ParameterTypeError(
"用于指定“全曲数据导出的类型”的参数,其类型须为`Path`路径或字符串,而非`{}`类型的`{}`值".format(
type(filepath_or_format), filepath_or_format
)
)
return self._get_io_plugin_by_format(
self._music_output_plugins, filepath_or_format
)
def get_track_output_plugin_by_format(
self, filepath_or_format: Union[Path, str]
) -> Generator[TrackOutputPluginBase, None, None]:
"""通过指定输出的文件或格式,以获取对应的导出单个音轨用插件"""
if isinstance(filepath_or_format, str):
return (
plugin
for plugin in self._track_output_plugins
if plugin.can_handle_format(filepath_or_format)
return self._get_io_plugin_by_format(
self._track_output_plugins, filepath_or_format
)
def _get_plugin_by_name(
self,
plugin_regdict: Mapping[str, T_Plugin],
plugin_name: str,
plugin_usage: str = "",
) -> T_Plugin:
"""通过指定名称,以获取对应的插件,当名称重叠时,取版本号最大的"""
try:
return max(
[
plugin
for plugin in plugin_regdict.values()
if plugin.metainfo.name == plugin_name
],
key=lambda plugin: plugin.metainfo.version,
)
elif isinstance(filepath_or_format, Path):
return (
plugin
for plugin in self._track_output_plugins
if plugin.can_handle_file(filepath_or_format)
)
else:
raise ParameterTypeError(
"用于指定“单音轨数据导出的类型”的参数,其类型须为`Path`路径或字符串,而非`{}`类型的`{}`值".format(
type(filepath_or_format), filepath_or_format
)
except ValueError:
raise PluginInstanceNotFoundError(
"未找到“用于{}、名为`{}`”的插件".format(plugin_usage, plugin_name)
)
def get_music_input_plugin(self, plugin_name: str) -> MusicInputPluginBase:
"""获取指定名称的全曲导入用插件,当名称重叠时,取版本号最大的"""
try:
return max(
[
plugin
for plugin in self._music_input_plugins
if plugin.metainfo.name == plugin_name
],
key=lambda plugin: plugin.metainfo.version,
)
except ValueError:
raise PluginInstanceNotFoundError(
"未找到“用于导入曲目、名为`{}`”的插件".format(plugin_name)
)
return self._get_plugin_by_name(
self._music_input_plugins, plugin_name, "导入全曲"
)
def get_track_input_plugin(self, plugin_name: str) -> TrackInputPluginBase:
"""获取指定名称的单音轨导入用插件,当名称重叠时,取版本号最大的"""
try:
return max(
[
plugin
for plugin in self._track_input_plugins
if plugin.metainfo.name == plugin_name
],
key=lambda plugin: plugin.metainfo.version,
)
except ValueError:
raise PluginInstanceNotFoundError(
"未找到“用于导入单个音轨、名为`{}`”的插件".format(plugin_name)
)
return self._get_plugin_by_name(
self._track_input_plugins, plugin_name, "导入单轨"
)
def get_music_operate_plugin(self, plugin_name: str) -> MusicOperatePluginBase:
"""获取指定名称的全曲处理用插件,当名称重叠时,取版本号最大的"""
try:
return max(
[
plugin
for plugin in self._music_operate_plugins
if plugin.metainfo.name == plugin_name
],
key=lambda plugin: plugin.metainfo.version,
)
except ValueError:
raise PluginInstanceNotFoundError(
"未找到“用于处理整个曲目、名为`{}`”的插件".format(plugin_name)
)
return self._get_plugin_by_name(
self._music_operate_plugins, plugin_name, "处理整个曲目"
)
def get_track_operate_plugin(self, plugin_name: str) -> TrackOperatePluginBase:
"""获取指定名称的单音轨处理用插件,当名称重叠时,取版本号最大的"""
try:
return max(
[
plugin
for plugin in self._track_operate_plugins
if plugin.metainfo.name == plugin_name
],
key=lambda plugin: plugin.metainfo.version,
)
except ValueError:
raise PluginInstanceNotFoundError(
"未找到“用于处理单个音轨、名为`{}`”的插件".format(plugin_name)
)
return self._get_plugin_by_name(
self._track_operate_plugins, plugin_name, "处理单个音轨"
)
def get_music_output_plugin(self, plugin_name: str) -> MusicOutputPluginBase:
"""获取指定名称的导出全曲用插件,当名称重叠时,取版本号最大的"""
try:
return max(
[
plugin
for plugin in self._music_output_plugins
if plugin.metainfo.name == plugin_name
],
key=lambda plugin: plugin.metainfo.version,
)
except ValueError:
raise PluginMetainfoNotFoundError(
"未找到“用于导出完整曲目、名为`{}`”的插件".format(plugin_name)
)
return self._get_plugin_by_name(
self._music_output_plugins, plugin_name, "导出完整曲目"
)
def get_track_output_plugin(self, plugin_name: str) -> TrackOutputPluginBase:
"""获取指定名称的导出单音轨用插件,当名称重叠时,取版本号最大的"""
try:
return max(
[
plugin
for plugin in self._track_output_plugins
if plugin.metainfo.name == plugin_name
],
key=lambda plugin: plugin.metainfo.version,
)
except ValueError:
raise PluginMetainfoNotFoundError(
"未找到“用于导出单个音轨、名为`{}`”的插件".format(plugin_name)
)
return self._get_plugin_by_name(
self._track_output_plugins, plugin_name, "导出单个音轨"
)
def get_service_plugin(self, plugin_name: str) -> ServicePluginBase:
"""获取服务用插件,当名称重叠时,取版本号最大的"""
try:
return max(
[
plugin
for plugin in self._service_plugins
if plugin.metainfo.name == plugin_name
],
key=lambda plugin: plugin.metainfo.version,
)
except ValueError:
raise PluginInstanceNotFoundError(
"未找到名为`{}`的服务用插件".format(plugin_name)
)
return self._get_plugin_by_name(self._service_plugins, plugin_name, "提供服务")
def get_library_plugin(self, plugin_name: str) -> LibraryPluginBase:
"""获取依赖库类插件,当名称重叠时,取版本号最高的"""
try:
return max(
[
plugin
for plugin in self._library_plugins
if plugin.metainfo.name == plugin_name
],
key=lambda plugin: plugin.metainfo.version,
)
except ValueError:
raise PluginInstanceNotFoundError(
"未找到名为`{}`的依赖库插件".format(plugin_name)
)
return self._get_plugin_by_name(
self._library_plugins, plugin_name, "作为依赖库"
)
def supported_input_formats(self) -> Set[str]:
"""所有支持的导入格式"""
@@ -402,7 +335,8 @@ class PluginRegistry:
chain.from_iterable(
plugin.supported_formats
for plugin in chain(
self._music_input_plugins, self._track_input_plugins
self._music_input_plugins.values(),
self._track_input_plugins.values(),
)
)
)
@@ -413,7 +347,8 @@ class PluginRegistry:
chain.from_iterable(
plugin.supported_formats
for plugin in chain(
self._music_output_plugins, self._track_output_plugins
self._music_output_plugins.values(),
self._track_output_plugins.values(),
)
)
)
@@ -423,97 +358,67 @@ _global_plugin_registry = PluginRegistry()
"""全局插件注册表实例"""
def music_input_plugin(plugin_id: str):
"""全曲输入用插件装饰器"""
def __plugin_regist_decorator(plg_id: str, rgst_func: Callable[[type, str], None]):
def decorator(cls):
global _global_plugin_registry
cls.id = plugin_id
_global_plugin_registry.register_music_input_plugin(cls)
cls.id = plg_id
rgst_func(cls, plg_id)
return cls
return decorator
def music_input_plugin(plugin_id: str):
"""全曲输入用插件装饰器"""
return __plugin_regist_decorator(
plugin_id, _global_plugin_registry.register_music_input_plugin
)
def track_input_plugin(plugin_id: str):
"""单轨输入用插件装饰器"""
def decorator(cls):
global _global_plugin_registry
cls.id = plugin_id
_global_plugin_registry.register_track_input_plugin(cls)
return cls
return decorator
return __plugin_regist_decorator(
plugin_id, _global_plugin_registry.register_track_input_plugin
)
def music_operate_plugin(plugin_id: str):
"""全曲处理用插件装饰器"""
def decorator(cls):
global _global_plugin_registry
cls.id = plugin_id
_global_plugin_registry.register_music_operate_plugin(cls)
return cls
return decorator
return __plugin_regist_decorator(
plugin_id, _global_plugin_registry.register_music_operate_plugin
)
def track_operate_plugin(plugin_id: str):
"""音轨处理插件装饰器"""
def decorator(cls):
global _global_plugin_registry
cls.id = plugin_id
_global_plugin_registry.register_track_operate_plugin(cls)
return cls
return decorator
return __plugin_regist_decorator(
plugin_id, _global_plugin_registry.register_track_operate_plugin
)
def music_output_plugin(plugin_id: str):
"""乐曲输出用插件装饰器"""
def decorator(cls):
global _global_plugin_registry
cls.id = plugin_id
_global_plugin_registry.register_music_output_plugin(cls)
return cls
return decorator
return __plugin_regist_decorator(
plugin_id, _global_plugin_registry.register_music_output_plugin
)
def track_output_plugin(plugin_id: str):
"""音轨输出用插件装饰器"""
def decorator(cls):
global _global_plugin_registry
cls.id = plugin_id
_global_plugin_registry.register_track_output_plugin(cls)
return cls
return decorator
return __plugin_regist_decorator(
plugin_id, _global_plugin_registry.register_track_output_plugin
)
def service_plugin(plugin_id: str):
"""服务插件装饰器"""
def decorator(cls):
global _global_plugin_registry
cls.id = plugin_id
_global_plugin_registry.register_service_plugin(cls)
return cls
return decorator
return __plugin_regist_decorator(
plugin_id, _global_plugin_registry.register_service_plugin
)
def library_plugin(plugin_id: str):
"""支持库插件装饰器"""
def decorator(cls):
global _global_plugin_registry
cls.id = plugin_id
_global_plugin_registry.register_library_plugin(cls)
return cls
return decorator
return __plugin_regist_decorator(
plugin_id, _global_plugin_registry.register_library_plugin
)

View File

@@ -14,5 +14,11 @@ print(msct:=MusiCreater.import_music(Path("./resources/测试片段.mid")))
print(msct.music)
# 为了让类型检查器满意,以下方法不建议使用,因为这本质上是越过了 MusiCreater 类而直接执行插件的函数
print(t := msct.midi_2_music_plugin.load(Path("./resources/测试片段.mid"), None))
# 我们建议用这种方式来代替
t = _global_plugin_registry._music_input_plugins["midi_2_music_plugin"].load(Path("./resources/测试片段.mid"), None)
print(_global_plugin_registry)
print(msct._plugin_cache)
print(msct.midi_2_music_plugin.load(Path("./resources/测试片段.mid"), None))