mirror of
https://github.com/TriM-Organization/Musicreater.git
synced 2026-07-24 01:02:32 +00:00
634 lines
18 KiB
Python
634 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
音·创 v3 的插件基类,提供抽象接口以供实际插件使用
|
||
"""
|
||
|
||
"""
|
||
版权所有 © 2026 金羿
|
||
Copyright © 2026 Eilles
|
||
|
||
开源相关声明请见 仓库根目录下的 License.md
|
||
Terms & Conditions: License.md in the root directory
|
||
"""
|
||
|
||
# 睿乐组织 开发交流群 861684859
|
||
# Email TriM-Organization@hotmail.com
|
||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||
|
||
# =====================
|
||
# NOTE: [WARNING]
|
||
# 这个文件是一坨屎山代码
|
||
# 请勿模仿,请多包容
|
||
# =====================
|
||
|
||
|
||
import sys
|
||
|
||
from abc import ABC, abstractmethod
|
||
from dataclasses import dataclass, field
|
||
from enum import Enum
|
||
from pathlib import Path
|
||
from typing import (
|
||
Dict,
|
||
Any,
|
||
Optional,
|
||
List,
|
||
Tuple,
|
||
Union,
|
||
Sequence,
|
||
BinaryIO,
|
||
Generator,
|
||
Iterator,
|
||
Set,
|
||
Type,
|
||
Mapping,
|
||
overload,
|
||
)
|
||
|
||
if sys.version_info >= (3, 11):
|
||
import tomllib
|
||
import tomli_w
|
||
else:
|
||
import tomli as tomllib # 第三方包
|
||
import tomli_w
|
||
|
||
from .exceptions import (
|
||
PluginConfigDumpError,
|
||
PluginConfigLoadError,
|
||
PluginMetainfoNotFoundError,
|
||
PluginMetainfoTypeError,
|
||
PluginMetainfoValueError,
|
||
PluginAttributeNotFoundError,
|
||
ParameterTypeError,
|
||
PluginInstanceNotFoundError,
|
||
)
|
||
from .data import SingleMusic, SingleTrack
|
||
|
||
# 已经全部由 plugins.py 提供接口
|
||
# 请用户从 plugins.py 导入
|
||
# 不要在这里导,会坏掉的
|
||
|
||
# __all__ = [
|
||
# # 枚举类
|
||
# "PluginType",
|
||
# # 抽象基类/数据类(插件参数定义)
|
||
# "PluginConfig",
|
||
# "PluginMetaInformation",
|
||
# # 抽象基类(插件定义)
|
||
# "MusicInputPlugin",
|
||
# "TrackInputPlugin",
|
||
# "MusicOperatePlugin",
|
||
# "TrackOperatePlugin",
|
||
# "MusicOutputPlugin",
|
||
# "TrackOutputPlugin",
|
||
# "ServicePlugin",
|
||
# "LibraryPlugin",
|
||
# # 插件注册用装饰函数
|
||
# "music_input_plugin",
|
||
# "track_input_plugin",
|
||
# "music_operate_plugin",
|
||
# "track_operate_plugin",
|
||
# "music_output_plugin",
|
||
# "track_output_plugin",
|
||
# "service_plugin",
|
||
# "library_plugin",
|
||
# ]
|
||
|
||
|
||
# ========================
|
||
# 枚举类
|
||
# ========================
|
||
|
||
|
||
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: Mapping[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:
|
||
raise PluginConfigDumpError(
|
||
"插件配置文件类型不应为`{}`,须为`TOML`格式。".format(file_path.suffix)
|
||
)
|
||
|
||
try:
|
||
with file_path.open("wb") as f:
|
||
tomli_w.dump(self.to_dict(), f, multiline_strings=False, indent=4)
|
||
except Exception as e:
|
||
raise PluginConfigDumpError("插件配置文件无法保存。") from e
|
||
|
||
@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))
|
||
except Exception as e:
|
||
raise PluginConfigLoadError("插件配置文件无法加载。") from e
|
||
|
||
|
||
@dataclass
|
||
class PluginMetaInformation(ABC):
|
||
"""插件元信息"""
|
||
|
||
name: str
|
||
"""插件名称"""
|
||
author: str
|
||
"""插件作者"""
|
||
description: str
|
||
"""插件简介"""
|
||
version: Tuple[int, ...]
|
||
"""插件版本号"""
|
||
type: PluginTypes
|
||
"""插件类型"""
|
||
license: str = "MIT License"
|
||
"""插件发布时采用的许可协议"""
|
||
dependencies: Sequence[str] = tuple()
|
||
"""插件是否对其他插件存在依赖"""
|
||
|
||
def similar_to(self, other: "PluginMetaInformation") -> bool:
|
||
"""判断两个插件元信息是否相似
|
||
|
||
参数
|
||
====
|
||
other: PluginMetaInformation
|
||
待比较的插件元信息
|
||
|
||
返回
|
||
====
|
||
bool
|
||
两个插件元信息是否相似
|
||
"""
|
||
return (self.name == other.name) and (self.type == other.type)
|
||
|
||
|
||
# ========================
|
||
# 抽象基类
|
||
# ========================
|
||
|
||
|
||
class TopPluginBase(ABC):
|
||
"""所有插件的抽象基类"""
|
||
|
||
metainfo: PluginMetaInformation
|
||
"""插件元信息"""
|
||
|
||
def __init_subclass__(cls) -> None:
|
||
super().__init_subclass__()
|
||
if hasattr(cls, "metainfo"):
|
||
if not isinstance(cls.metainfo, PluginMetaInformation):
|
||
raise PluginMetainfoTypeError(
|
||
"类`{cls_name}`之属性`metainfo`的类型,必须为`PluginMetaInformation`".format(
|
||
cls_name=cls.__name__
|
||
)
|
||
)
|
||
else:
|
||
if not cls.__name__.endswith("PluginBase"):
|
||
raise PluginMetainfoNotFoundError(
|
||
"类`{cls_name}`必须定义一个`metainfo`属性。".format(
|
||
cls_name=cls.__name__
|
||
)
|
||
)
|
||
|
||
|
||
class TopInOutPluginBase(TopPluginBase, ABC):
|
||
"""导入导出用抽象基类"""
|
||
|
||
supported_formats: Tuple[str, ...] = tuple()
|
||
"""支持的格式(定义后会自动转大写)"""
|
||
|
||
def __init_subclass__(cls) -> None:
|
||
super().__init_subclass__()
|
||
|
||
if hasattr(cls, "supported_formats"):
|
||
if cls.supported_formats:
|
||
# 强制转换为大写,并使用元组
|
||
cls.supported_formats = tuple(map(str.upper, cls.supported_formats))
|
||
else:
|
||
cls.supported_formats = tuple()
|
||
else:
|
||
raise PluginAttributeNotFoundError(
|
||
"用于导入导出数据的类`{cls_name}`必须定义一个`supported_formats`属性。".format(
|
||
cls_name=cls.__name__
|
||
)
|
||
)
|
||
|
||
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)
|
||
|
||
|
||
class MusicInputPluginBase(TopInOutPluginBase, ABC):
|
||
"""导入用插件抽象基类-完整曲目"""
|
||
|
||
def __init_subclass__(cls) -> None:
|
||
super().__init_subclass__()
|
||
|
||
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__,
|
||
cls_type=cls.metainfo.type.name,
|
||
)
|
||
)
|
||
|
||
@abstractmethod
|
||
def loadbytes(self, bytes_buffer_in: BinaryIO, config) -> "SingleMusic":
|
||
"""从字节流加载数据到完整曲目
|
||
|
||
参数
|
||
====
|
||
bytes_buffer_in: BinaryIO
|
||
输入的二进制字节流
|
||
config: Optional[PluginConfig]
|
||
插件配置;**可选**
|
||
|
||
返回
|
||
====
|
||
SingleMusic
|
||
解析得到的完整曲目对象
|
||
"""
|
||
...
|
||
|
||
def load(self, file_path: Path, config) -> "SingleMusic":
|
||
"""从文件加载数据到完整曲目
|
||
|
||
参数
|
||
====
|
||
file_path: Path
|
||
输入文件路径
|
||
config: Optional[PluginConfig]
|
||
插件配置;**可选**
|
||
|
||
返回
|
||
====
|
||
SingleMusic
|
||
解析得到的完整曲目对象
|
||
"""
|
||
with file_path.open("rb") as f:
|
||
return self.loadbytes(f, config)
|
||
|
||
|
||
class TrackInputPluginBase(TopInOutPluginBase, ABC):
|
||
"""导入用插件抽象基类-单个音轨"""
|
||
|
||
def __init_subclass__(cls) -> None:
|
||
super().__init_subclass__()
|
||
|
||
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__,
|
||
cls_type=cls.metainfo.type.name,
|
||
)
|
||
)
|
||
|
||
@abstractmethod
|
||
def loadbytes(self, bytes_buffer_in: BinaryIO, config) -> "SingleTrack":
|
||
"""从字节流加载音符数据到单个音轨
|
||
|
||
参数
|
||
====
|
||
bytes_buffer_in: BinaryIO
|
||
输入的二进制字节流
|
||
config: Optional[PluginConfig]
|
||
插件配置;**可选**
|
||
|
||
返回
|
||
====
|
||
SingleTrack
|
||
解析得到的单个音轨对象
|
||
"""
|
||
...
|
||
|
||
def load(self, file_path: Path, config) -> "SingleTrack":
|
||
"""从文件加载音符数据到单个音轨
|
||
|
||
参数
|
||
====
|
||
file_path: Path
|
||
输入文件路径
|
||
config: Optional[PluginConfig]
|
||
插件配置;**可选**
|
||
|
||
返回
|
||
====
|
||
SingleTrack
|
||
解析得到的单个音轨对象
|
||
"""
|
||
with file_path.open("rb") as f:
|
||
return self.loadbytes(f, config)
|
||
|
||
|
||
class MusicOperatePluginBase(TopPluginBase, ABC):
|
||
"""音乐处理用插件抽象基类-完整曲目"""
|
||
|
||
def __init_subclass__(cls) -> None:
|
||
super().__init_subclass__()
|
||
|
||
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__,
|
||
cls_type=cls.metainfo.type.name,
|
||
)
|
||
)
|
||
|
||
@abstractmethod
|
||
def process(self, data: "SingleMusic", config) -> "SingleMusic":
|
||
"""处理完整曲目的数据
|
||
|
||
参数
|
||
====
|
||
data: SingleMusic
|
||
待处理的完整曲目
|
||
config: Optional[PluginConfig]
|
||
插件配置;**可选**
|
||
|
||
返回
|
||
====
|
||
SingleMusic
|
||
处理后的完整曲目
|
||
"""
|
||
...
|
||
|
||
|
||
class TrackOperatePluginBase(TopPluginBase, ABC):
|
||
"""音乐处理用插件抽象基类-单个音轨"""
|
||
|
||
def __init_subclass__(cls) -> None:
|
||
super().__init_subclass__()
|
||
|
||
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__,
|
||
cls_type=cls.metainfo.type.name,
|
||
)
|
||
)
|
||
|
||
@abstractmethod
|
||
def process(self, data: "SingleTrack", config) -> "SingleTrack":
|
||
"""处理单个音轨的音符数据
|
||
|
||
参数
|
||
====
|
||
data: SingleTrack
|
||
待处理的单个音轨
|
||
config: Optional[PluginConfig]
|
||
插件配置;**可选**
|
||
|
||
返回
|
||
====
|
||
SingleTrack
|
||
处理后的单个音轨
|
||
"""
|
||
...
|
||
|
||
|
||
class MusicOutputPluginBase(TopInOutPluginBase, ABC):
|
||
"""导出用插件的抽象基类-完整曲目"""
|
||
|
||
def __init_subclass__(cls) -> None:
|
||
super().__init_subclass__()
|
||
|
||
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__,
|
||
cls_type=cls.metainfo.type.name,
|
||
)
|
||
)
|
||
|
||
@abstractmethod
|
||
def stream_dump(self, data: "SingleMusic", config) -> Iterator[bytes]:
|
||
"""将完整曲目导出为对应格式的字节流
|
||
|
||
参数
|
||
====
|
||
data: SingleMusic
|
||
待导出的完整曲目
|
||
config: Optional[PluginConfig]
|
||
插件配置;**可选**
|
||
|
||
返回
|
||
====
|
||
Iterator[bytes]
|
||
分块导出的二进制字节串
|
||
"""
|
||
...
|
||
|
||
def dump(self, data: "SingleMusic", file_path: Path, config):
|
||
"""将完整曲目导出为对应格式的文件
|
||
|
||
参数
|
||
====
|
||
data: SingleMusic
|
||
待导出的完整曲目
|
||
file_path: Path
|
||
输出文件路径
|
||
config: Optional[PluginConfig]
|
||
插件配置;**可选**
|
||
"""
|
||
|
||
with file_path.open("wb") as f:
|
||
for _bytes in self.stream_dump(data, config):
|
||
f.write(_bytes)
|
||
|
||
|
||
class TrackOutputPluginBase(TopInOutPluginBase, ABC):
|
||
"""导出用插件的抽象基类-单个音轨"""
|
||
|
||
def __init_subclass__(cls) -> None:
|
||
super().__init_subclass__()
|
||
|
||
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__,
|
||
cls_type=cls.metainfo.type.name,
|
||
)
|
||
)
|
||
|
||
@abstractmethod
|
||
def stream_dump(self, data: "SingleTrack", config) -> Iterator[bytes]:
|
||
"""将单个音轨导出为对应格式的字节流
|
||
|
||
参数
|
||
====
|
||
data: SingleTrack
|
||
待导出的单个音轨
|
||
config: Optional[PluginConfig]
|
||
插件配置;**可选**
|
||
|
||
返回
|
||
====
|
||
Iterator[bytes]
|
||
分块导出的二进制字节串
|
||
"""
|
||
...
|
||
|
||
def dump(self, data: "SingleTrack", file_path: Path, config):
|
||
"""将单个音轨导出为对应格式的文件
|
||
|
||
参数
|
||
====
|
||
data: SingleTrack
|
||
待导出的单个音轨
|
||
file_path: Path
|
||
输出文件路径
|
||
config: Optional[PluginConfig]
|
||
插件配置;**可选**
|
||
"""
|
||
with file_path.open("wb") as f:
|
||
for _bytes in self.stream_dump(data, config):
|
||
f.write(_bytes)
|
||
|
||
|
||
class ServicePluginBase(TopPluginBase, ABC):
|
||
"""服务插件抽象基类"""
|
||
|
||
def __init_subclass__(cls) -> None:
|
||
super().__init_subclass__()
|
||
|
||
if cls.metainfo.type != PluginTypes.SERVICE:
|
||
raise PluginMetainfoValueError(
|
||
"插件类`{cls_name}`是从`ServicePlugin`继承的,该类的子类应当为一个`PluginType.SERVICE`类型的插件,而不是`PluginType.{cls_type}`".format(
|
||
cls_name=cls.__name__,
|
||
cls_type=cls.metainfo.type.name,
|
||
)
|
||
)
|
||
|
||
@abstractmethod
|
||
def serve(self, config=None) -> None:
|
||
"""服务插件的运行逻辑
|
||
|
||
参数
|
||
====
|
||
config: Optional[PluginConfig]
|
||
插件配置;**可选**
|
||
"""
|
||
...
|
||
|
||
|
||
class LibraryPluginBase(TopPluginBase, ABC):
|
||
"""插件依赖库的抽象基类"""
|
||
|
||
def __init_subclass__(cls) -> None:
|
||
super().__init_subclass__()
|
||
|
||
if cls.metainfo.type != PluginTypes.LIBRARY:
|
||
raise PluginMetainfoValueError(
|
||
"插件类`{cls_name}`是从`LibraryPlugin`继承的,该类的子类应当为一个`PluginType.LIBRARY`类型的插件,而不是`PluginType.{cls_type}`".format(
|
||
cls_name=cls.__name__,
|
||
cls_type=cls.metainfo.type.name,
|
||
)
|
||
)
|
||
|
||
# 怎么?
|
||
# 插件的依赖项就不需要什么调用了吧
|