优化插件基类、完善插件文档

This commit is contained in:
2026-02-13 02:10:03 +08:00
parent 295da53c60
commit 62cd4a0c94
6 changed files with 348 additions and 154 deletions

View File

@@ -498,9 +498,9 @@ class MusicOutputPluginBase(TopInOutPluginBase, ABC):
)
@abstractmethod
def dumpbytes(
def stream_dump(
self, data: "SingleMusic", config: Optional[PluginConfig]
) -> BinaryIO:
) -> Iterator[bytes]:
"""将完整曲目导出为对应格式的字节流
参数
@@ -512,12 +512,11 @@ class MusicOutputPluginBase(TopInOutPluginBase, ABC):
返回
====
BinaryIO
导出的二进制字节
Iterator[bytes]
分块导出的二进制字节
"""
pass
@abstractmethod
def dump(
self, data: "SingleMusic", file_path: Path, config: Optional[PluginConfig]
):
@@ -533,7 +532,9 @@ class MusicOutputPluginBase(TopInOutPluginBase, ABC):
插件配置;**可选**
"""
pass
with file_path.open("wb") as f:
for _bytes in self.stream_dump(data, config):
f.write(_bytes)
class TrackOutputPluginBase(TopInOutPluginBase, ABC):
@@ -551,9 +552,9 @@ class TrackOutputPluginBase(TopInOutPluginBase, ABC):
)
@abstractmethod
def dumpbytes(
def stream_dump(
self, data: "SingleTrack", config: Optional[PluginConfig]
) -> BinaryIO:
) -> Iterator[bytes]:
"""将单个音轨导出为对应格式的字节流
参数
@@ -565,12 +566,11 @@ class TrackOutputPluginBase(TopInOutPluginBase, ABC):
返回
====
BinaryIO
导出的二进制字节
Iterator[bytes]
分块导出的二进制字节
"""
pass
@abstractmethod
def dump(
self, data: "SingleTrack", file_path: Path, config: Optional[PluginConfig]
):
@@ -585,7 +585,9 @@ class TrackOutputPluginBase(TopInOutPluginBase, ABC):
config: Optional[PluginConfig]
插件配置;**可选**
"""
pass
with file_path.open("wb") as f:
for _bytes in self.stream_dump(data, config):
f.write(_bytes)
class ServicePluginBase(TopPluginBase, ABC):
@@ -603,15 +605,13 @@ class ServicePluginBase(TopPluginBase, ABC):
)
@abstractmethod
def serve(self, config: Optional[PluginConfig], *args) -> None:
def serve(self, config: Optional[PluginConfig]) -> None:
"""服务插件的运行逻辑
参数
====
config: Optional[PluginConfig]
插件配置;**可选**
*args: Any
其他运行时参数
"""
pass

184
docs/编写插件.md Normal file
View File

@@ -0,0 +1,184 @@
# 示例插件:导入音符数据
> 版权所有 © 2026 金羿
> Copyright © 2026 Eilles
睿乐组织 开发交流群 [861684859](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=fxNYIX_zKMgaO8X6K7pP7tHtLB7JRvdX&noverify=0&group_code=861684859)
Email [TriM-Organization@hotmail.com](mailto:TriM-Organization@hotmail.com)
```license
本示例模块开放授权同时本教程文件已开放至公共领域
请注意
若是对本文件的直接转载在形式上没有修改增删添加注释或单纯修改排版翻译录屏截图
则该使用者需要在转载所及之处明确在转载的内容开头标注本文之原始著作权人
在当前文件下该原始著作权人为金羿(Eilles)
如果是对本文进行了一定程度上的修改和补充或者以不同方式演绎本文件如制成视频教程等
则无需标注原作者允许该使用者自行署名
本声明仅限于包含此声明的本文件本声明与项目内其他文件无关
```
本教程文档的关联文件是
- 全曲导入音轨导入插件示例[exp_importdata_plugin.py](../examples/exp_importdata_plugin.py)
- 导出曲目导出音轨插件示例[exp_dataexport_plugin.py](../examples/exp_dataexport_plugin.py)
## 新建文件
### 基础模块知识
首先一个 **· v3** 的插件应当存储于一个 Python 模块之中也就是插件存在于可以被 import 语句引入的 module
这就意味着承载插件的模块本质上可以是多个 Python `.py` 文件组成的带有 `__init__.py` 的一个文件夹
或者是一个简单的 `.py` 文件
我们有这种共识大家已经知道了模块的相关知识后面的教程中你已经理解 **· v3** 插件和 Python 模块的区别
## 开始动笔
### 插件配置
如果插件需要配置项则需进行此节
`Musicreater.plugins` 导入 `PluginConfig` 并从此继承一个类且须用 dataclass 装饰器来注册之这就成为了一个插件的配置类
_对于这个 `dataclass` 数据类的使用方式可以阅读 dataclass 的官方文档或者直接在实例后面打个 `.`直接看看能干哈子_
```python
from Musicreater.plugins import PluginConfig
from dataclasses import dataclass
@dataclass
class ExampleImportConfig(PluginConfig):
example_config_item3: bool
example_config_item1: str = "example_config_item"
example_config_item2: int = 0
```
## 编写插件
### 导入所需项目
首先在代码开头导入插件所需的东西
在此之前我们明确一个 **· v3** 的插件应当是一个继承自我们已经准备好的插件基类的****缩句插件是类
**· v3** 任何对音乐的操作包括**导入****处理****导出**都分为对 **整首曲目** 的操作和对 **单个音轨** 的操作
在这里我们首先要对插件的类型进行判别根据以下表格可以得出所用功能对应的插件类型
| 功能\对象 | 完整曲目 | 单个音轨 |
|----------|----------|----------|
| 导入数据 | `PluginTypes.FUNCTION_MUSIC_IMPORT` | `PluginTypes.FUNCTION_TRACK_IMPORT` |
| 数据处理 | `PluginTypes.FUNCTION_MUSIC_OPERATE` | `PluginTypes.FUNCTION_TRACK_OPERATE` |
| 导出数据 | `PluginTypes.FUNCTION_MUSIC_EXPORT` | `PluginTypes.FUNCTION_TRACK_EXPORT` |
| 支持库 | `PluginTypes.LIBRARY` | 同左 |
| 提供服务 | `PluginTypes.SERVICE` | 同左 |
也就是说除了 `PluginTypes.LIBRARY` `PluginTypes.SERVICE` 是不按照处理对象做区分的外其余的这些都是对数据进行处理的插件因此是做了处理数据的类型区分的
我们对每个不同类型的插件都做了专用的抽象基类和一个装饰器函数因为插件本身就是类所以对应类型的插件只需要继承我们提供的抽象基类并通过装饰器函数注册即可具体写法在后面会说哦
也就是说如果我们要写的是一个用来导入音乐的对整个曲目进行处理的插件那么就需要导入 `MusicInputPluginBase` 类和 `music_input_plugin` 函数以便后续调用
同时既然要导入内容那就一并把 `PluginMetaInformation` 类和 `PluginTypes` 类也导入了吧这是定义插件的信息所需要的也就是说这样的话我们在导入部分就应该这样写
```python
from Musicreater.plugins import (
music_input_plugin,
PluginMetaInformation,
PluginTypes,
MusicInputPluginBase,
)
```
### 定义信息
接着我们来定义一个插件的信息并将其注册
假设我们想要做一个对**整首曲目**进行**导入操作**的插件参照前面举的例子那么就需要继承 `MusicInputPluginBase`
> 请注意插件类的类名称不得以 `Base` 结尾因为咱写的是插件不是插件基类
在插件的类的开头需要用插件注册装饰函数来对插件类装饰
```python
@music_input_plugin("example_import_plugin")
class xxx:
...
```
我们这里对应插件类型的注册器是 `music_input_plugin` 函数
在注册器函数后的参数是这个插件的惟一识别码不应与其他任何插件混淆
通常这个惟一识别码可以是这个插件的功能描述或者就是插件名
接着编写这个插件也即是此类
每个插件的类必须包含一个用于指定插件元信息的 `metainfo` 属性
如果插件是导入数据或者导出数据的插件则必须包含一个 `supported_formats` 属性用以声明插件所支持的数据格式
对于插件的元信息我们规定为一个 `PluginMetaInformation` 实例这个实例需要的参数如下
```python
# 注册插件
@music_input_plugin("something_convert_to_music")
# 继承自对应类型的插件基类
class ExampleImportPlugin(MusicInputPluginBase):
# 插件元信息定义
metainfo = PluginMetaInformation(
name="示例导入插件", # 插件名称
author="金羿", # 插件作者
description="这是一个示例导入插件", # 插件描述
version=(0, 0, 1), # 插件版本
type=PluginTypes.FUNCTION_MUSIC_IMPORT, # 插件类型,需要和注册的类型与继承的基类相符合
license="The Unlicense", # 插件许可证(可缺省,默认为字符串 `MIT License`
dependencies=("something_convertion_library") # 插件对于其他插件的依赖项(可缺省,默认为空元组)
)
```
对于实现导入导出数据的功能的插件`supported_formats` 属性应当是一个元组其中最好以全字母大写的字符串形式列出支持的**文件格式**或者**数据格式**如果定义的时候没有大写的话内部会自动处理成大写的所以插件类的实例后面也会变成大写这个时候因为原定义是小写有可能造成混淆所以尽量不要写小写例如一个处理 `.mp4` 文件格式的插件可以这样写
```python
@...
class ...:
...
supported_formats = ("MP4", "MPEG4", "MPEG-4")
```
至此你已经完成了插件基本信息的定义
### 实现功能
根据插件的类型不同每个插件都需要实现至少一个指定的方法如下表所示
| 插件功能 | 必须实现的方法 | 类型描述 | 可选实现的方法| 可选方法类型描述 | 备注 |
| ------ | ------------ | - | ----------- | - |----|
| 导入数据 | `loadbytes` | `Callable[[BinaryIO, Optional[PluginConfig]], T@插件处理对象类型]` | `load` | `Callable[[Path, Optional[PluginConfig]], T@插件处理对象类型]` | 如果 `load` 方法不单独实现则会自动在打开文件后将文件 IO 变量传入 `loadbytes` 中并返回之 |
| 数据处理 | `process` | `Callable[[T@插件处理对象类型, Optional[PluginConfig]], T@插件处理对象类型]` | | | 根据处理对象是完整曲目`SingleMusic`还是单个音轨`SingleTrack`返回也是一样的导入导出数据相关的插件亦皆同此说 |
| 导出数据 | `stream_dump` | `Callable[[T@插件处理对象类型, Optional[PluginConfig]], Iterator[bytes]]` | `dump` | `Callable[[T@插件处理对象类型, Path, Optional[PluginConfig]], None]` | 若未重写 `dump` 方法基类已提供默认实现逐块写入 `stream_dump` 的结果 |
| 支持库 | | | | | |
| 服务 | `serve` | `Callable[[Optional[PluginConfig]], None]` | | | 用于提供后台服务或一次性任务由运行时调用暂无设计思路相关讨论请见[项目待办清单](../TO-DO.md#讨论) |
也就是说举个例子一个**用于导入**的插件类必须包含一个 `loadbytes` 方法用于从字节流中导入数据可选是否单独实现 `load` 方法如果不单独实现则在调用时会直接通过打开文件后传参数给 `loadbytes` 来实现
```python
@...
class ExampleImportPlugin(MusicInputPluginBase):
...
# 定义 loadbytes 方法,从字节流中导入数据
def loadbytes(
self, bytes_buffer_in: BinaryIO, config: Optional[ExampleImportConfig]
) -> "SingleMusic":
... # 这里写功能实现
# 插件可选地定义 load 方法,从文件导入数据。下面展示的是不定义 load 方法时候的实现方式
def load(
self, file_path: Path, config: Optional[ExampleImportConfig]
) -> "SingleMusic":
with file_path.open("rb") as f:
return self.loadbytes(f, config)
```
至此一个插件的编写已经完成
同时如果有不清楚的地方可以查看我们的[内置插件](../Musicreater/builtin_plugins/)说不定会给你一些启发

View File

@@ -1,130 +0,0 @@
# 示例插件:导入音符数据
> 版权所有 © 2026 金羿
> Copyright © 2026 Eilles
睿乐组织 开发交流群 [861684859](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=fxNYIX_zKMgaO8X6K7pP7tHtLB7JRvdX&noverify=0&group_code=861684859)
Email [TriM-Organization@hotmail.com](mailto:TriM-Organization@hotmail.com)
```license
本示例模块开放授权同时本教程文件已开放至公共领域
请注意
若是对本文件的直接转载在形式上没有修改增删添加注释或单纯修改排版翻译录屏截图
则该使用者需要在转载所及之处明确在转载的内容开头标注本文之原始著作权人
在当前文件下该原始著作权人为金羿(Eilles)
如果是对本文进行了一定程度上的修改和补充或者以不同方式演绎本文件如制成视频教程等
则无需标注原作者允许该使用者自行署名
本声明仅限于包含此声明的本文件本声明与项目内其他文件无关
```
本教程文档的关联文件是[exp_importdata_plugin.py](./exp_importdata_plugin.py)
## 新建文件夹 · 基础模块知识
首先一个 **· v3** 的插件应当存储于一个 Python 模块之中也就是插件存在于可以被 import 语句引入的 module
这就意味着承载插件的模块本质上可以是多个 Python `.py` 文件组成的带有 `__init__.py` 的一个文件夹
或者是一个简单的 `.py` 文件
我们有这种共识大家已经知道了模块的相关知识后面的教程中你已经理解 **· v3** 插件和 Python 模块的区别
## 开始编写插件 · 插件基础
首先导入插件所需的类
在这里我们是一个用来导入数据的插件
所以就需要导入 `MusicInputPluginBase` 类和 `music_input_plugin` 函数
同时`PluginMetaInformation` 类和 `PluginTypes` 类也必须导入这是插件的元信息所需要的
```python
from Musicreater.plugins import (
music_input_plugin,
PluginMetaInformation,
PluginTypes,
MusicInputPluginBase,
)
```
如果插件需要配置那么请再导入 `PluginConfig` 并从此继承一个类且须用 dataclass 装饰器来注册之
_对于这个类的使用方式可以阅读 dataclass 的官方文档_
```python
from Musicreater.plugins import PluginConfig
from dataclasses import dataclass
@dataclass
class ExampleImportConfig(PluginConfig):
example_config_item3: bool
example_config_item1: str = "example_config_item"
example_config_item2: int = 0
```
## 编写插件 · 开始
接着我们来制作一个插件
首先一个 **· v3** 的插件应当是一个继承自我们已经准备好的插件基类的****缩句插件是类
**· v3** 任何对音乐的操作包括导入导出处理都分为对 **整首曲目** 的操作和对 **单个音轨** 的操作
我们的样例是一个对**整首曲目**进行**导入操作**的插件因此需要继承 `MusicInputPluginBase`
插件类的类名称不得以 `Base` 结尾因为咱写的是插件不是插件基类
在插件的类的开头需要用插件注册装饰函数来对插件类装饰
```python
@music_input_plugin("example_import_plugin")
class xxx:
...
```
我们这里对应插件类型的注册器是 `music_input_plugin` 函数
在注册器函数后的参数是这个插件的惟一识别码不应与其他插件混淆
通常可以是这个插件的功能描述或者就是插件名
接着编写这个插件也即是此类
每个插件的类必须包含一个用于指定插件元信息的 `metainfo` 属性
如果插件是导入数据或者导出数据的插件则必须包含一个 `supported_formats` 属性用以声明插件所支持的数据格式
用于导入的插件类必须包含一个 `loadbytes` 方法用于从字节流中导入数据可选是否单独实现 `load` 方法如果不单独实现则在调用时会直接通过打开文件后使用 `loadbytes` 的方式实现
```python
# 注册插件
@music_input_plugin("something_convert_to_music")
# 继承自对应类型的插件基类
class ExampleImportPlugin(MusicInputPluginBase):
# 插件元信息定义
metainfo = PluginMetaInformation(
name="示例导入插件", # 插件名称
author="金羿", # 插件作者
description="这是一个示例导入插件", # 插件描述
version=(0, 0, 1), # 插件版本
type=PluginTypes.FUNCTION_MUSIC_IMPORT, # 插件类型
license="The Unlicense", # 插件许可证
dependencies=("something_convertion_library") # 插件对于其他插件的依赖项(此功能尚未实现)
)
# 导入导出插件支持的数据格式,大小写皆可,无需加后缀名前的那个点
supported_formats = ("EXP", "example_format")
# 定义 loadbytes 方法,从字节流中导入数据
def loadbytes(
self, bytes_buffer_in: BinaryIO, config: Optional[ExampleImportConfig]
) -> "SingleMusic":
...
# 插件可选地定义 load 方法,从文件导入数据。下面展示的是不定义 load 方法时候的实现方式
def load(
self, file_path: Path, config: Optional[ExampleImportConfig]
) -> "SingleMusic":
with file_path.open("rb") as f:
return self.loadbytes(f, config)
```
至此一个插件的编写已经完成
同时如果有不清楚的地方可以查看我们的[内置插件](../Musicreater/builtin_plugins/)说不定会给你一些启发

View File

@@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
"""
示例插件:导出成其他文件
"""
"""
版权所有 © 2026 金羿
Copyright © 2026 Eilles
"""
# 睿乐组织 开发交流群 861684859
# Email TriM-Organization@hotmail.com
"""
本示例模块开放授权,本文件已开放至公共领域。
请注意:
若是对本文件的直接转载(在形式上没有修改、增删、添加注释,或单纯修改排版、翻译、录屏、截图)
则该使用者需要在转载所及之处,明确在转载的内容开头标注本文之原始著作权人
在当前文件下,该原始著作权人为金羿(Eilles)
如果是对本文进行了一定程度上的修改和补充、或者以不同方式演绎本文件(如制成视频教程等)
则无需标注原作者,允许该使用者自行署名
本声明仅限于包含此声明的本文件,本声明与项目内其他文件无关。
"""
from typing import BinaryIO, Optional, Iterator, Generator, Any, Tuple
from pathlib import Path
from dataclasses import dataclass
from Musicreater import SingleMusic, SingleTrack
from Musicreater.plugins import (
PluginConfig,
PluginMetaInformation,
PluginTypes,
music_output_plugin,
MusicOutputPluginBase,
track_output_plugin,
TrackOutputPluginBase,
)
@dataclass
class ExampleExportConfig(PluginConfig):
example_config_item3: bool
example_config_item1: str = "example_config_item"
example_config_item2: int = 0
def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
for k, v in self.to_dict().items():
yield k, v
@music_output_plugin("convert_music_to_something")
class ExampleExportMusicPlugin(MusicOutputPluginBase):
metainfo = PluginMetaInformation(
name="示例导出插件·甲",
author="金羿",
description="这是一个示例导出插件,演示整首曲目导出到其他文件格式的插件的编写过程",
version=(0, 0, 1),
type=PluginTypes.FUNCTION_MUSIC_EXPORT,
license="The Unlicense",
dependencies=("something_convertion_library"),
)
supported_formats = ("EXP", "EXAMPLE_FORMAT")
@staticmethod
def something_data_convert(*args) -> bytes:
return b"This is something wonderful"
def stream_dump(
self, data: SingleMusic, config: ExampleExportConfig | None
) -> Iterator[bytes]:
if not config:
config = ExampleExportConfig(True)
for cfg in config:
yield self.something_data_convert(cfg)
# 插件可选地定义 dump 方法,从文件导入数据。下面展示的是不定义 load 方法时候的实现方式
def dump(
self, data: SingleMusic, file_path: Path, config: ExampleExportConfig | None
):
with file_path.open("wb") as f:
for _bytes in self.stream_dump(data, config):
f.write(_bytes)
@track_output_plugin("convert_track_to_something")
class ExampleImportTrackPlugin(TrackOutputPluginBase):
metainfo = PluginMetaInformation(
name="示例导出插件·乙",
author="金羿",
description="这是一个示例导出插件,演示从音轨导出的其他格式的插件的编写过程",
version=(0, 0, 1),
type=PluginTypes.FUNCTION_TRACK_EXPORT,
license="The Unlicense",
# 可以缺省依赖,如果不需要的话
)
supported_formats = ("EXP", "example_format")
def stream_dump(
self, data: SingleTrack, config: ExampleExportConfig | None
) -> Iterator[bytes]:
if not config:
config = ExampleExportConfig(True)
for cfg in config:
yield ExampleExportMusicPlugin.something_data_convert(cfg)
# 可以缺省 dump 方法,会直接用上面展示过的方法输出

View File

@@ -28,13 +28,15 @@ from typing import BinaryIO, Optional
from pathlib import Path
from dataclasses import dataclass
from Musicreater import SingleMusic
from Musicreater import SingleMusic, SingleTrack
from Musicreater.plugins import (
music_input_plugin,
PluginConfig,
PluginMetaInformation,
PluginTypes,
music_input_plugin,
MusicInputPluginBase,
track_input_plugin,
TrackInputPluginBase,
)
@@ -46,18 +48,18 @@ class ExampleImportConfig(PluginConfig):
@music_input_plugin("something_convert_to_music")
class ExampleImportPlugin(MusicInputPluginBase):
class ExampleImportMusicPlugin(MusicInputPluginBase):
metainfo = PluginMetaInformation(
name="示例导入插件",
name="示例导入插件·甲",
author="金羿",
description="这是一个示例导入插件",
description="这是一个示例导入插件,演示导入到全曲的插件编写过程",
version=(0, 0, 1),
type=PluginTypes.FUNCTION_MUSIC_IMPORT,
license="The Unlicense",
dependencies=("something_convertion_library"),
)
supported_formats = ("EXP", "example_format")
supported_formats = ("EXP", "EXAMPLE_FORMAT")
def loadbytes(
self, bytes_buffer_in: BinaryIO, config: Optional[ExampleImportConfig]
@@ -70,3 +72,25 @@ class ExampleImportPlugin(MusicInputPluginBase):
) -> "SingleMusic":
with file_path.open("rb") as f:
return self.loadbytes(f, config)
@track_input_plugin("something_convert_to_track")
class ExampleImportTrackPlugin(TrackInputPluginBase):
metainfo = PluginMetaInformation(
name="示例导入插件·乙",
author="金羿",
description="这是一个示例导入插件,演示导入到音轨的插件编写过程",
version=(0, 0, 1),
type=PluginTypes.FUNCTION_TRACK_IMPORT,
license="The Unlicense",
# 可以缺省依赖,如果不需要的话
)
supported_formats = ("EXP", "example_format")
def loadbytes(
self, bytes_buffer_in: BinaryIO, config: Optional[ExampleImportConfig]
) -> "SingleTrack":
return SingleTrack()
# 可以缺省 load 方法,会直接用上面展示过的方法读取数据

View File

@@ -14,9 +14,9 @@ print(msct := MusiCreater.import_music(Path("./resources/测试片段.mid")))
print(msct.music)
# 如果要直接访问插件里面的函数:
# 为了确保类型安全,以下方法不建议使用,因为这本质上是越过了 MusiCreater 类而直接执行插件的函数
print(t := msct.midi_2_music_plugin.load(Path("./resources/测试片段.mid"), None))
print(t := msct.midi_2_music_plugin.load(Path("./resources/测试片段.mid"), None)) # type: ignore
# 我们建议用这种方式来代替
t = _global_plugin_registry._music_input_plugins["midi_2_music_plugin"].load(
Path("./resources/测试片段.mid"),
@@ -28,8 +28,12 @@ t = _global_plugin_registry._music_input_plugins["midi_2_music_plugin"].load(
from Musicreater.plugins import MusicInputPluginBase
if isinstance((p := msct.midi_2_music_plugin), MusicInputPluginBase):
# 但是,我们不建议用这样的方式读取操作配置类,尽管这也是可以的
t = p.load(Path("./resources/测试片段.mid"), None)
# 但是说实话,既然已经在 MusiCreater 类中提供了
# import_music、export_music、perform_operation_on_music 等方法,
# 那么我们不建议使用上面展示的调取插件的方式来执行插件内的函数。
msct.perform_operation_on_music
print(_global_plugin_registry)
print(msct._plugin_cache)