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

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

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/)说不定会给你一些启发