mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-02 05:17:23 +08:00
feat(module): add dynamic module switches and catalogs
This commit is contained in:
@@ -532,6 +532,8 @@ API_EXTENDED_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = (
|
||||
_spec("system.network.targets"),
|
||||
_spec("system.network.test", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT),
|
||||
_spec("system.module.list"),
|
||||
_spec("system.module.catalog"),
|
||||
_admin_read("system.module.settings"),
|
||||
_spec("system.module.test", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT),
|
||||
_spec(
|
||||
"plugin.market.sync_wiki",
|
||||
@@ -781,6 +783,8 @@ API_OPERATION_ROUTES: dict[str, ApiOperationRoute] = {
|
||||
"system.network.targets": ApiOperationRoute("GET", "/api/v1/system/nettest/targets"),
|
||||
"system.network.test": ApiOperationRoute("GET", "/api/v1/system/nettest"),
|
||||
"system.module.list": ApiOperationRoute("GET", "/api/v1/system/modulelist"),
|
||||
"system.module.catalog": ApiOperationRoute("GET", "/api/v1/system/module-catalog"),
|
||||
"system.module.settings": ApiOperationRoute("GET", "/api/v1/system/module-settings"),
|
||||
"system.module.test": ApiOperationRoute("GET", "/api/v1/system/moduletest/{moduleid}"),
|
||||
"plugin.market.sync_wiki": ApiOperationRoute("POST", "/api/v1/system/setting/PLUGIN_MARKET/sync-wiki"),
|
||||
"plugin.runtime.status": ApiOperationRoute("GET", "/api/v1/plugin/runtime"),
|
||||
|
||||
@@ -179,8 +179,10 @@ OPERATION_DESCRIPTIONS = {
|
||||
"subscription.user.list": "List public subscriptions owned by one accessible MoviePilot username.",
|
||||
"subtitle.search.media": "Search subtitle providers for one canonical media identity and optional season or episode.",
|
||||
"subtitle.search.title": "Search subtitle providers from a free-form title and optional media filters.",
|
||||
"system.module.list": "List loaded MoviePilot module IDs and localized names.",
|
||||
"system.module.test": "Run the built-in availability test for one loaded MoviePilot module.",
|
||||
"system.module.list": "List enabled MoviePilot module IDs and localized names.",
|
||||
"system.module.catalog": "List MoviePilot host modules and service-type options for UI selectors.",
|
||||
"system.module.settings": "List built-in MoviePilot modules that can be enabled or disabled by an administrator.",
|
||||
"system.module.test": "Run the built-in availability test for one enabled MoviePilot module.",
|
||||
"system.network.targets": "List approved built-in network-test targets without exposing their request URLs.",
|
||||
"system.network.test": "Test connectivity to one approved target or the legacy constrained URL input.",
|
||||
"torrent.cache.clear": "Delete every cached torrent context.",
|
||||
|
||||
@@ -13919,7 +13919,23 @@
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "List loaded MoviePilot module IDs and localized names. Method: GET. Path: /api/v1/system/modulelist. Effect: safe_read.",
|
||||
"description": "List MoviePilot host modules and service-type options for UI selectors. Method: GET. Path: /api/v1/system/module-catalog. Effect: safe_read.",
|
||||
"properties": {
|
||||
"operation_id": {
|
||||
"const": "system.module.catalog",
|
||||
"description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"operation_id"
|
||||
],
|
||||
"title": "system.module.catalog",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "List enabled MoviePilot module IDs and localized names. Method: GET. Path: /api/v1/system/modulelist. Effect: safe_read.",
|
||||
"properties": {
|
||||
"operation_id": {
|
||||
"const": "system.module.list",
|
||||
@@ -13935,7 +13951,23 @@
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "Run the built-in availability test for one loaded MoviePilot module. Method: GET. Path: /api/v1/system/moduletest/{moduleid}. Effect: external_side_effect.",
|
||||
"description": "List built-in MoviePilot modules that can be enabled or disabled by an administrator. Method: GET. Path: /api/v1/system/module-settings. Effect: safe_read.",
|
||||
"properties": {
|
||||
"operation_id": {
|
||||
"const": "system.module.settings",
|
||||
"description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"operation_id"
|
||||
],
|
||||
"title": "system.module.settings",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "Run the built-in availability test for one enabled MoviePilot module. Method: GET. Path: /api/v1/system/moduletest/{moduleid}. Effect: external_side_effect.",
|
||||
"properties": {
|
||||
"operation_id": {
|
||||
"const": "system.module.test",
|
||||
@@ -13944,7 +13976,7 @@
|
||||
},
|
||||
"path_params": {
|
||||
"additionalProperties": false,
|
||||
"description": "Resource identity placeholders for system.module.test. Run the built-in availability test for one loaded MoviePilot module. Use only the named fields below.",
|
||||
"description": "Resource identity placeholders for system.module.test. Run the built-in availability test for one enabled MoviePilot module. Use only the named fields below.",
|
||||
"properties": {
|
||||
"moduleid": {
|
||||
"description": "Exact loaded module ID returned by system.module.list.",
|
||||
@@ -15849,7 +15881,9 @@
|
||||
"subscription.user.list",
|
||||
"subtitle.search.media",
|
||||
"subtitle.search.title",
|
||||
"system.module.catalog",
|
||||
"system.module.list",
|
||||
"system.module.settings",
|
||||
"system.module.test",
|
||||
"system.network.targets",
|
||||
"system.network.test",
|
||||
|
||||
@@ -15,6 +15,7 @@ from app.api.response import (
|
||||
)
|
||||
from app.application.classification.runtime import ClassificationRuntime
|
||||
from app.application.configuration import get_api_runtime_config_snapshot
|
||||
from app.application.module import get_module_manager
|
||||
from app.chain.media import MediaChain
|
||||
from app.chain.scraping import ScrapingChain
|
||||
from app.chain.tmdb import TmdbChain
|
||||
@@ -66,12 +67,45 @@ _BUILTIN_MEDIA_SOURCES = (
|
||||
),
|
||||
)
|
||||
|
||||
_BUILTIN_MEDIA_SOURCE_MODULES = {
|
||||
MediaSource.TMDB: "TheMovieDbModule",
|
||||
MediaSource.Douban: "DoubanModule",
|
||||
MediaSource.Bangumi: "BangumiModule",
|
||||
MediaSource.AniList: "AniListModule",
|
||||
MediaSource.IMDb: "ImdbModule",
|
||||
MediaSource.TVDB: "TheTvDbModule",
|
||||
MediaSource.MusicBrainz: "MusicBrainzModule",
|
||||
MediaSource.TheAudioDB: "TheAudioDbModule",
|
||||
MediaSource.DoubanMusic: "DoubanModule",
|
||||
}
|
||||
|
||||
|
||||
def _enabled_builtin_media_sources() -> set[MediaSource]:
|
||||
"""按宿主模块启用状态返回可供前端选择的内置媒体来源。"""
|
||||
try:
|
||||
enabled_module_ids = {
|
||||
spec.id for spec in get_module_manager().list_enabled_specs()
|
||||
}
|
||||
except RuntimeError:
|
||||
# 兼容应用组合根尚未装配时的直接调用,正式请求始终走已装配的运行目录。
|
||||
return set(_BUILTIN_MEDIA_SOURCE_MODULES)
|
||||
return {
|
||||
media_source
|
||||
for media_source, module_id in _BUILTIN_MEDIA_SOURCE_MODULES.items()
|
||||
if module_id in enabled_module_ids
|
||||
}
|
||||
|
||||
|
||||
def _registered_media_sources() -> list[_SchemaMediaSourceInfo]:
|
||||
"""合并内置与启用插件声明的媒体来源,并按来源标识去重。"""
|
||||
from app.application.plugin.runtime import get_plugin_manager
|
||||
|
||||
result = list(_BUILTIN_MEDIA_SOURCES)
|
||||
enabled_builtin_sources = _enabled_builtin_media_sources()
|
||||
result = [
|
||||
source
|
||||
for source in _BUILTIN_MEDIA_SOURCES
|
||||
if source.media_source in enabled_builtin_sources
|
||||
]
|
||||
seen = {source.media_source for source in result}
|
||||
for raw_source in get_plugin_manager().get_media_sources():
|
||||
try:
|
||||
|
||||
@@ -26,16 +26,28 @@ from app.chain.media import MediaChain
|
||||
from app.chain.storage import StorageChain
|
||||
from app.chain.transfer.facade import TransferChain
|
||||
from app.foundation import text as text_tools
|
||||
from app.runtime.localization import LocaleHelper
|
||||
from app.runtime.progress import ProgressHelper
|
||||
from app.schemas.common import ManageRequest as _SchemaManageRequest
|
||||
from app.schemas.response import Response as _SchemaResponse
|
||||
from app.schemas.storage import StorageCatalogOption as _SchemaStorageCatalogOption
|
||||
from app.schemas.storage import StorageOption as _SchemaStorageOption
|
||||
from app.schemas.system import TransferDirectoryConf as _SchemaTransferDirectoryConf
|
||||
from app.schemas.types import ProgressKey
|
||||
from app.schemas.types import ProgressKey, StorageSchema
|
||||
from app.schemas.workflow import FileItem as _SchemaFileItem
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
|
||||
_STORAGE_CATALOG_META = {
|
||||
StorageSchema.Local.value: ("本地", "mdi-folder-multiple-outline", False),
|
||||
StorageSchema.Alipan.value: ("阿里云盘", "mdi-cloud-outline", True),
|
||||
StorageSchema.U115.value: ("115网盘", "mdi-cloud-outline", True),
|
||||
StorageSchema.Rclone.value: ("RClone", "mdi-server-network-outline", True),
|
||||
StorageSchema.Alist.value: ("OpenList", "mdi-server-network-outline", True),
|
||||
StorageSchema.AlistGo.value: ("AList", "mdi-server-network-outline", True),
|
||||
StorageSchema.SMB.value: ("SMB网络共享", "mdi-folder-network-outline", True),
|
||||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/directories",
|
||||
@@ -119,6 +131,32 @@ def storage_options(
|
||||
]
|
||||
|
||||
|
||||
@router.get(
|
||||
"/catalog",
|
||||
summary="查询存储类型目录",
|
||||
response_model=List[_SchemaStorageCatalogOption],
|
||||
)
|
||||
def storage_catalog(
|
||||
_: ApiPrincipal = Depends(get_current_active_user),
|
||||
page: CompatiblePageParam = None,
|
||||
count: CompatibleCountParam = None,
|
||||
) -> List[_SchemaStorageCatalogOption]:
|
||||
"""返回所有可新增存储类型及其展示元数据,不包含连接配置和凭据。"""
|
||||
return [
|
||||
_SchemaStorageCatalogOption(
|
||||
type=storage_type,
|
||||
name=default_name,
|
||||
name_i18n=LocaleHelper.translate(
|
||||
f"storage.{storage_type}",
|
||||
default=default_name,
|
||||
),
|
||||
icon=icon,
|
||||
remote=remote,
|
||||
)
|
||||
for storage_type, (default_name, icon, remote) in _STORAGE_CATALOG_META.items()
|
||||
]
|
||||
|
||||
|
||||
@router.post("/manage", summary="网盘存储统一管理", response_model=_SchemaResponse[Dict[str, Any]]) # type: ignore[misc]
|
||||
def manage(request: _SchemaManageRequest, _: ApiPrincipal = Depends(get_current_active_superuser)) -> Any:
|
||||
"""
|
||||
|
||||
@@ -44,6 +44,11 @@ from app.domain.metainfo import MetaInfo
|
||||
from app.foundation.crypto import HashUtils
|
||||
from app.foundation.environment import is_free_threaded_runtime, is_gil_enabled
|
||||
from app.runtime.execution import run_in_threadpool_to_completion
|
||||
from app.runtime.extensions.module.adapter import (
|
||||
capture_host_module_config,
|
||||
should_expose_host_module_option,
|
||||
should_run_host_module,
|
||||
)
|
||||
from app.runtime.localization import LocaleHelper
|
||||
from app.runtime.log import logger
|
||||
from app.runtime.progress import AsyncProgressHelper
|
||||
@@ -61,7 +66,9 @@ from app.schemas.system import PluginMarketSyncData as _SchemaPluginMarketSyncDa
|
||||
from app.schemas.system import PluginMarketSyncRequest as _SchemaPluginMarketSyncRequest
|
||||
from app.schemas.system import RuleTestData as _SchemaRuleTestData
|
||||
from app.schemas.system import SystemEnvironmentUpdateData as _SchemaSystemEnvironmentUpdateData
|
||||
from app.schemas.system import SystemModuleCatalogListData as _SchemaSystemModuleCatalogListData
|
||||
from app.schemas.system import SystemModuleListData as _SchemaSystemModuleListData
|
||||
from app.schemas.system import SystemModuleSettingListData as _SchemaSystemModuleSettingListData
|
||||
from app.schemas.system import SystemSettingsUpdateRequest as _SchemaSystemSettingsUpdateRequest
|
||||
from app.schemas.system import SystemUpdateRequest as _SchemaSystemUpdateRequest
|
||||
from app.schemas.system import SystemUpdateStatus as _SchemaSystemUpdateStatus
|
||||
@@ -87,6 +94,24 @@ _PUBLIC_SYSTEM_CONFIG_KEYS = {
|
||||
}
|
||||
_PUBLIC_SETTINGS_KEYS = {"PLUGIN_MARKET"}
|
||||
|
||||
_MODULE_DESCRIPTION_FALLBACKS = {
|
||||
"downloader": "下载器适配器,负责创建和管理下载任务。",
|
||||
"indexer": "站点索引模块,负责搜索站点资源并解析结果。",
|
||||
"mediarecognize": "媒体数据源,负责媒体识别、搜索或探索数据。",
|
||||
"mediaserver": "媒体服务器适配器,负责同步媒体库并执行媒体操作。",
|
||||
"notification": "消息通知通道,负责发送系统事件和任务结果。",
|
||||
"other": "媒体处理或运行基础模块。",
|
||||
}
|
||||
|
||||
|
||||
def _module_description(module_id: str, name: str, module_type: str) -> str:
|
||||
"""读取模块的本地化职责说明,并为新增模块按类型提供可读兜底。"""
|
||||
fallback = _MODULE_DESCRIPTION_FALLBACKS.get(module_type, f"{name} 模块。")
|
||||
return LocaleHelper.translate(
|
||||
f"system.modules.{module_id}.description",
|
||||
default=fallback,
|
||||
)
|
||||
|
||||
|
||||
def _database_backup_artifact_data(artifact: Any) -> _SchemaDatabaseBackupArtifactData:
|
||||
"""将内部备份制品映射为不含宿主路径的 Web DTO。"""
|
||||
@@ -973,15 +998,15 @@ async def nettest(
|
||||
|
||||
@router.get(
|
||||
"/modulelist",
|
||||
summary="查询已加载的模块ID列表",
|
||||
summary="查询已启用的模块ID列表",
|
||||
response_model=_SchemaResponse[_SchemaSystemModuleListData],
|
||||
)
|
||||
def modulelist(_: _SchemaTokenPayload = Depends(verify_token)):
|
||||
"""
|
||||
查询已加载的模块ID列表
|
||||
查询当前配置下应参与健康检查的模块ID列表
|
||||
"""
|
||||
modules = []
|
||||
for spec in get_module_manager().list_specs():
|
||||
for spec in get_module_manager().list_enabled_specs():
|
||||
module_id = spec.id
|
||||
name = str(spec.metadata["name"])
|
||||
modules.append(
|
||||
@@ -998,6 +1023,86 @@ def modulelist(_: _SchemaTokenPayload = Depends(verify_token)):
|
||||
return _SchemaResponse(success=True, data={"modules": modules})
|
||||
|
||||
|
||||
@router.get(
|
||||
"/module-catalog",
|
||||
summary="查询宿主模块目录",
|
||||
response_model=_SchemaResponse[_SchemaSystemModuleCatalogListData],
|
||||
)
|
||||
def module_catalog(_: _SchemaTokenPayload = Depends(verify_token)):
|
||||
"""返回模块及其服务类型目录,供前端选择器统一构造选项。"""
|
||||
manager = get_module_manager()
|
||||
specs = manager.list_specs()
|
||||
snapshot = capture_host_module_config(specs)
|
||||
modules = []
|
||||
for spec in specs:
|
||||
name = str(spec.metadata["name"])
|
||||
selector = spec.selector
|
||||
option_value = None
|
||||
if selector is not None and selector.kind == "system_config_item":
|
||||
option_value = str(selector.config["match_value"])
|
||||
modules.append(
|
||||
{
|
||||
"id": spec.id,
|
||||
"name": name,
|
||||
"name_i18n": LocaleHelper.translate(
|
||||
f"system.modules.{spec.id}.name",
|
||||
default=name,
|
||||
),
|
||||
"name_key": f"system.modules.{spec.id}.name",
|
||||
"description_i18n": _module_description(
|
||||
spec.id,
|
||||
name,
|
||||
str(spec.metadata.get("type", "")),
|
||||
),
|
||||
"description_key": f"system.modules.{spec.id}.description",
|
||||
"type": str(spec.metadata["type"]),
|
||||
"subtype": str(spec.metadata["subtype"]),
|
||||
"option_value": option_value,
|
||||
"enabled": should_expose_host_module_option(spec, snapshot),
|
||||
"active": should_run_host_module(spec, snapshot),
|
||||
}
|
||||
)
|
||||
return _SchemaResponse(success=True, data={"modules": modules})
|
||||
|
||||
|
||||
@router.get(
|
||||
"/module-settings",
|
||||
summary="查询可手动开关的内置模块",
|
||||
response_model=_SchemaResponse[_SchemaSystemModuleSettingListData],
|
||||
)
|
||||
async def module_settings(
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
):
|
||||
"""查询没有其它激活配置、可由用户统一控制的内置模块。"""
|
||||
configured = get_runtime_settings().get("MODULE_ENABLE", {})
|
||||
if not isinstance(configured, dict):
|
||||
configured = {}
|
||||
|
||||
modules = []
|
||||
for spec in get_module_manager().list_switchable_specs():
|
||||
module_id = spec.id
|
||||
name = str(spec.metadata["name"])
|
||||
modules.append(
|
||||
{
|
||||
"id": module_id,
|
||||
"name": name,
|
||||
"name_i18n": LocaleHelper.translate(
|
||||
f"system.modules.{module_id}.name",
|
||||
default=name,
|
||||
),
|
||||
"name_key": f"system.modules.{module_id}.name",
|
||||
"description_i18n": _module_description(
|
||||
module_id,
|
||||
name,
|
||||
str(spec.metadata.get("type", "")),
|
||||
),
|
||||
"description_key": f"system.modules.{module_id}.description",
|
||||
"enabled": configured.get(module_id, True) is not False,
|
||||
}
|
||||
)
|
||||
return _SchemaResponse(success=True, data={"modules": modules})
|
||||
|
||||
|
||||
@router.get("/moduletest/{moduleid}", summary="模块可用性测试", response_model=_SchemaResponse[None])
|
||||
def moduletest(moduleid: str, _: _SchemaTokenPayload = Depends(verify_token)):
|
||||
"""
|
||||
|
||||
@@ -2,104 +2,169 @@
|
||||
"system": {
|
||||
"modules": {
|
||||
"BangumiModule": {
|
||||
"name": "Bangumi"
|
||||
"name": "Bangumi",
|
||||
"description": "Bangumi anime data source for media identification, search, and discovery."
|
||||
},
|
||||
"AmllModule": {
|
||||
"name": "AMLL TTML",
|
||||
"description": "Uses AMLL TTML as a lyrics source for music scraping."
|
||||
},
|
||||
"AniListModule": {
|
||||
"name": "AniList",
|
||||
"description": "AniList anime data source for media identification, search, and discovery."
|
||||
},
|
||||
"DiscordModule": {
|
||||
"name": "Discord"
|
||||
"name": "Discord",
|
||||
"description": "Sends system events and task results to Discord."
|
||||
},
|
||||
"DoubanModule": {
|
||||
"name": "Douban"
|
||||
"name": "Douban",
|
||||
"description": "Douban data source for movie, TV, and music identification, search, and discovery lists."
|
||||
},
|
||||
"EmbyModule": {
|
||||
"name": "Emby"
|
||||
"name": "Emby",
|
||||
"description": "Media-server adapter that synchronizes Emby libraries and runs refresh or playback actions."
|
||||
},
|
||||
"FanartModule": {
|
||||
"name": "Fanart"
|
||||
"name": "Fanart",
|
||||
"description": "Adds Fanart.tv posters, backdrops, and artwork during media scraping."
|
||||
},
|
||||
"FeishuModule": {
|
||||
"name": "Feishu"
|
||||
"name": "Feishu",
|
||||
"description": "Sends system events and task results to Feishu bots."
|
||||
},
|
||||
"FileManagerModule": {
|
||||
"name": "File Organization"
|
||||
"name": "File Organization",
|
||||
"description": "Executes the file reads, copies, moves, renames, and deletions used by media organization."
|
||||
},
|
||||
"FilterModule": {
|
||||
"name": "Filter"
|
||||
"name": "Filter",
|
||||
"description": "Applies filter rules, recognition terms, and release-group matching during search, download, and organization."
|
||||
},
|
||||
"ImdbModule": {
|
||||
"name": "IMDb"
|
||||
"name": "IMDb",
|
||||
"description": "IMDb data source for movie and TV identification and search."
|
||||
},
|
||||
"IndexerModule": {
|
||||
"name": "Site Indexer"
|
||||
"name": "Site Indexer",
|
||||
"description": "Indexes sites and searches resources, supplying parsed results to download workflows."
|
||||
},
|
||||
"ListenBrainzModule": {
|
||||
"name": "ListenBrainz",
|
||||
"description": "Supplies ListenBrainz charts and recommendation data for the music discovery page."
|
||||
},
|
||||
"JellyfinModule": {
|
||||
"name": "Jellyfin"
|
||||
"name": "Jellyfin",
|
||||
"description": "Media-server adapter that synchronizes Jellyfin libraries and runs refresh or playback actions."
|
||||
},
|
||||
"MediaVaultModule": {
|
||||
"name": "MediaVault"
|
||||
"name": "MediaVault",
|
||||
"description": "Media-server adapter that connects to MediaVault and synchronizes its library."
|
||||
},
|
||||
"LrclibModule": {
|
||||
"name": "LRCLIB",
|
||||
"description": "Looks up LRCLIB lyrics during music scraping."
|
||||
},
|
||||
"MusicBrainzModule": {
|
||||
"name": "MusicBrainz",
|
||||
"description": "Core music identification data source for music search, recordings, releases, artists, and metadata."
|
||||
},
|
||||
"PlexModule": {
|
||||
"name": "Plex"
|
||||
"name": "Plex",
|
||||
"description": "Media-server adapter that synchronizes Plex libraries and runs refresh or playback actions."
|
||||
},
|
||||
"PostgreSQLModule": {
|
||||
"name": "PostgreSQL"
|
||||
"name": "PostgreSQL",
|
||||
"description": "Persists system settings, media, subscriptions, tasks, and other application data."
|
||||
},
|
||||
"QbittorrentModule": {
|
||||
"name": "qBittorrent"
|
||||
"name": "qBittorrent",
|
||||
"description": "Downloader adapter that connects to qBittorrent and creates, pauses, and manages tasks."
|
||||
},
|
||||
"QQBotModule": {
|
||||
"name": "QQ"
|
||||
"name": "QQ",
|
||||
"description": "Sends system events and task results to QQ bots."
|
||||
},
|
||||
"RedisModule": {
|
||||
"name": "Redis Cache"
|
||||
"name": "Redis Cache",
|
||||
"description": "Stores cache, queues, and cross-process runtime state for scheduling and live progress."
|
||||
},
|
||||
"RtorrentModule": {
|
||||
"name": "rTorrent"
|
||||
"name": "rTorrent",
|
||||
"description": "Downloader adapter that connects to rTorrent and creates, pauses, and manages tasks."
|
||||
},
|
||||
"SlackModule": {
|
||||
"name": "Slack"
|
||||
"name": "Slack",
|
||||
"description": "Sends system events and task results to Slack."
|
||||
},
|
||||
"SubtitleModule": {
|
||||
"name": "Site Subtitle"
|
||||
"name": "Site Subtitle",
|
||||
"description": "Searches sites for subtitles, downloads them, and feeds them into media organization."
|
||||
},
|
||||
"SynologyChatModule": {
|
||||
"name": "Synology Chat"
|
||||
"name": "Synology Chat",
|
||||
"description": "Sends system events and task results to Synology Chat."
|
||||
},
|
||||
"TelegramModule": {
|
||||
"name": "Telegram"
|
||||
"name": "Telegram",
|
||||
"description": "Sends system events and task results to Telegram bots."
|
||||
},
|
||||
"TheMovieDbModule": {
|
||||
"name": "TheMovieDb"
|
||||
"name": "TheMovieDb",
|
||||
"description": "Core movie and TV data source for identification, search, discovery, recommendations, people, and collections."
|
||||
},
|
||||
"TheTvDbModule": {
|
||||
"name": "TheTvDb"
|
||||
"name": "TheTvDb",
|
||||
"description": "TheTvDb data source for TV identification and auxiliary metadata."
|
||||
},
|
||||
"TheAudioDbModule": {
|
||||
"name": "TheAudioDB",
|
||||
"description": "TheAudioDB auxiliary data source for music identification and artist search."
|
||||
},
|
||||
"TransmissionModule": {
|
||||
"name": "Transmission"
|
||||
"name": "Transmission",
|
||||
"description": "Downloader adapter that connects to Transmission and creates, pauses, and manages tasks."
|
||||
},
|
||||
"TrimeMediaModule": {
|
||||
"name": "Trime Media"
|
||||
"name": "Trime Media",
|
||||
"description": "Media-server adapter that connects to Trime Media and synchronizes its library."
|
||||
},
|
||||
"UgreenModule": {
|
||||
"name": "UGREEN"
|
||||
"name": "UGREEN",
|
||||
"description": "Media-server adapter that connects to UGREEN Media and synchronizes its library."
|
||||
},
|
||||
"VoceChatModule": {
|
||||
"name": "VoceChat"
|
||||
"name": "VoceChat",
|
||||
"description": "Sends system events and task results to VoceChat."
|
||||
},
|
||||
"WebPushModule": {
|
||||
"name": "WebPush"
|
||||
"name": "WebPush",
|
||||
"description": "Pushes system messages to users through browser Web Push."
|
||||
},
|
||||
"WechatModule": {
|
||||
"name": "WeCom"
|
||||
"name": "WeCom",
|
||||
"description": "Sends system events and task results to WeCom bots."
|
||||
},
|
||||
"WechatClawBotModule": {
|
||||
"name": "WeChat ClawBot"
|
||||
"name": "WeChat ClawBot",
|
||||
"description": "Sends system events and task results to WeChat ClawBot."
|
||||
},
|
||||
"ZSpaceModule": {
|
||||
"name": "Zspace"
|
||||
"name": "Zspace",
|
||||
"description": "Media-server adapter that connects to Zspace and synchronizes its library."
|
||||
}
|
||||
},
|
||||
"module_test": {
|
||||
"unsupported": "Module does not support testing"
|
||||
},
|
||||
"storage": {
|
||||
"local": "Local",
|
||||
"alipan": "Aliyun Drive",
|
||||
"u115": "115 Cloud",
|
||||
"rclone": "RClone",
|
||||
"alist": "OpenList",
|
||||
"alistgo": "AList",
|
||||
"smb": "SMB Network Share"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -2,104 +2,169 @@
|
||||
"system": {
|
||||
"modules": {
|
||||
"BangumiModule": {
|
||||
"name": "Bangumi"
|
||||
"name": "Bangumi",
|
||||
"description": "媒体识别、搜索和探索页使用的 Bangumi 动画数据源。"
|
||||
},
|
||||
"AmllModule": {
|
||||
"name": "AMLL TTML",
|
||||
"description": "将 AMLL TTML 服务作为音乐歌词来源。"
|
||||
},
|
||||
"AniListModule": {
|
||||
"name": "AniList",
|
||||
"description": "媒体识别、搜索和探索页使用的 AniList 动画数据源。"
|
||||
},
|
||||
"DiscordModule": {
|
||||
"name": "Discord"
|
||||
"name": "Discord",
|
||||
"description": "将系统事件和任务结果发送到 Discord。"
|
||||
},
|
||||
"DoubanModule": {
|
||||
"name": "豆瓣"
|
||||
"name": "豆瓣",
|
||||
"description": "电影、电视剧和音乐识别、搜索及探索榜单使用的豆瓣数据源。"
|
||||
},
|
||||
"EmbyModule": {
|
||||
"name": "Emby"
|
||||
"name": "Emby",
|
||||
"description": "媒体服务器适配器:同步 Emby 媒体库并执行刷新、播放等操作。"
|
||||
},
|
||||
"FanartModule": {
|
||||
"name": "Fanart"
|
||||
"name": "Fanart",
|
||||
"description": "媒体刮削时补充 Fanart.tv 海报、背景和艺术图。"
|
||||
},
|
||||
"FeishuModule": {
|
||||
"name": "飞书"
|
||||
"name": "飞书",
|
||||
"description": "将系统事件和任务结果发送到飞书机器人。"
|
||||
},
|
||||
"FileManagerModule": {
|
||||
"name": "文件整理"
|
||||
"name": "文件整理",
|
||||
"description": "媒体整理流程实际执行文件读写、复制、移动、重命名和删除。"
|
||||
},
|
||||
"FilterModule": {
|
||||
"name": "过滤器"
|
||||
"name": "过滤器",
|
||||
"description": "在搜索、下载和整理流程中执行过滤规则、识别词和发布组匹配。"
|
||||
},
|
||||
"ImdbModule": {
|
||||
"name": "IMDb"
|
||||
"name": "IMDb",
|
||||
"description": "电影和电视剧识别、搜索使用的 IMDb 数据源。"
|
||||
},
|
||||
"IndexerModule": {
|
||||
"name": "站点索引"
|
||||
"name": "站点索引",
|
||||
"description": "负责站点索引和资源搜索,向下载流程输出站点资源。"
|
||||
},
|
||||
"ListenBrainzModule": {
|
||||
"name": "ListenBrainz",
|
||||
"description": "为音乐推荐页提供 ListenBrainz 榜单和推荐数据。"
|
||||
},
|
||||
"JellyfinModule": {
|
||||
"name": "Jellyfin"
|
||||
"name": "Jellyfin",
|
||||
"description": "媒体服务器适配器:同步 Jellyfin 媒体库并执行刷新、播放等操作。"
|
||||
},
|
||||
"MediaVaultModule": {
|
||||
"name": "MediaVault"
|
||||
"name": "MediaVault",
|
||||
"description": "媒体服务器适配器:连接 MediaVault 并同步媒体库信息。"
|
||||
},
|
||||
"LrclibModule": {
|
||||
"name": "LRCLIB",
|
||||
"description": "为音乐刮削提供 LRCLIB 歌词查询。"
|
||||
},
|
||||
"MusicBrainzModule": {
|
||||
"name": "MusicBrainz",
|
||||
"description": "音乐核心识别数据源:负责音乐搜索、录音、专辑和艺人识别及元数据。"
|
||||
},
|
||||
"PlexModule": {
|
||||
"name": "Plex"
|
||||
"name": "Plex",
|
||||
"description": "媒体服务器适配器:同步 Plex 媒体库并执行刷新、播放等操作。"
|
||||
},
|
||||
"PostgreSQLModule": {
|
||||
"name": "PostgreSQL"
|
||||
"name": "PostgreSQL",
|
||||
"description": "保存系统配置、媒体、订阅和任务等应用持久化数据。"
|
||||
},
|
||||
"QbittorrentModule": {
|
||||
"name": "Qbittorrent"
|
||||
"name": "Qbittorrent",
|
||||
"description": "下载器适配器:连接 qBittorrent 并创建、暂停和管理下载任务。"
|
||||
},
|
||||
"QQBotModule": {
|
||||
"name": "QQ"
|
||||
"name": "QQ",
|
||||
"description": "将系统事件和任务结果发送到 QQ 机器人。"
|
||||
},
|
||||
"RedisModule": {
|
||||
"name": "Redis缓存"
|
||||
"name": "Redis缓存",
|
||||
"description": "保存缓存、队列和跨进程运行状态,支撑任务调度与实时进度。"
|
||||
},
|
||||
"RtorrentModule": {
|
||||
"name": "Rtorrent"
|
||||
"name": "Rtorrent",
|
||||
"description": "下载器适配器:连接 rTorrent 并创建、暂停和管理下载任务。"
|
||||
},
|
||||
"SlackModule": {
|
||||
"name": "Slack"
|
||||
"name": "Slack",
|
||||
"description": "将系统事件和任务结果发送到 Slack。"
|
||||
},
|
||||
"SubtitleModule": {
|
||||
"name": "站点字幕"
|
||||
"name": "站点字幕",
|
||||
"description": "从站点搜索和下载字幕,并参与媒体整理流程。"
|
||||
},
|
||||
"SynologyChatModule": {
|
||||
"name": "Synology Chat"
|
||||
"name": "Synology Chat",
|
||||
"description": "将系统事件和任务结果发送到 Synology Chat。"
|
||||
},
|
||||
"TelegramModule": {
|
||||
"name": "Telegram"
|
||||
"name": "Telegram",
|
||||
"description": "将系统事件和任务结果发送到 Telegram 机器人。"
|
||||
},
|
||||
"TheMovieDbModule": {
|
||||
"name": "TheMovieDb"
|
||||
"name": "TheMovieDb",
|
||||
"description": "影视核心识别数据源:负责电影、电视剧搜索识别,以及探索、推荐、人物和合集数据。"
|
||||
},
|
||||
"TheTvDbModule": {
|
||||
"name": "TheTvDb"
|
||||
"name": "TheTvDb",
|
||||
"description": "电视剧识别和辅助信息使用的 TheTvDb 数据源。"
|
||||
},
|
||||
"TheAudioDbModule": {
|
||||
"name": "TheAudioDB",
|
||||
"description": "音乐识别和搜索使用的 TheAudioDB 辅助数据源。"
|
||||
},
|
||||
"TransmissionModule": {
|
||||
"name": "Transmission"
|
||||
"name": "Transmission",
|
||||
"description": "下载器适配器:连接 Transmission 并创建、暂停和管理下载任务。"
|
||||
},
|
||||
"TrimeMediaModule": {
|
||||
"name": "飞牛影视"
|
||||
"name": "飞牛影视",
|
||||
"description": "媒体服务器适配器:连接飞牛影视并同步媒体库信息。"
|
||||
},
|
||||
"UgreenModule": {
|
||||
"name": "绿联影视"
|
||||
"name": "绿联影视",
|
||||
"description": "媒体服务器适配器:连接绿联影视并同步媒体库信息。"
|
||||
},
|
||||
"VoceChatModule": {
|
||||
"name": "VoceChat"
|
||||
"name": "VoceChat",
|
||||
"description": "将系统事件和任务结果发送到 VoceChat。"
|
||||
},
|
||||
"WebPushModule": {
|
||||
"name": "WebPush"
|
||||
"name": "WebPush",
|
||||
"description": "通过浏览器 Web Push 向用户推送系统消息。"
|
||||
},
|
||||
"WechatModule": {
|
||||
"name": "企业微信"
|
||||
"name": "企业微信",
|
||||
"description": "将系统事件和任务结果发送到企业微信机器人。"
|
||||
},
|
||||
"WechatClawBotModule": {
|
||||
"name": "微信 ClawBot"
|
||||
"name": "微信 ClawBot",
|
||||
"description": "将系统事件和任务结果发送到微信 ClawBot。"
|
||||
},
|
||||
"ZSpaceModule": {
|
||||
"name": "极影视"
|
||||
"name": "极影视",
|
||||
"description": "媒体服务器适配器:连接极影视并同步媒体库信息。"
|
||||
}
|
||||
},
|
||||
"module_test": {
|
||||
"unsupported": "模块不支持测试"
|
||||
},
|
||||
"storage": {
|
||||
"local": "本地",
|
||||
"alipan": "阿里云盘",
|
||||
"u115": "115网盘",
|
||||
"rclone": "RClone",
|
||||
"alist": "OpenList",
|
||||
"alistgo": "AList",
|
||||
"smb": "SMB网络共享"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -2,104 +2,169 @@
|
||||
"system": {
|
||||
"modules": {
|
||||
"BangumiModule": {
|
||||
"name": "Bangumi"
|
||||
"name": "Bangumi",
|
||||
"description": "媒體識別、搜尋和探索頁使用的 Bangumi 動畫資料源。"
|
||||
},
|
||||
"AmllModule": {
|
||||
"name": "AMLL TTML",
|
||||
"description": "將 AMLL TTML 服務作為音樂歌詞來源。"
|
||||
},
|
||||
"AniListModule": {
|
||||
"name": "AniList",
|
||||
"description": "媒體識別、搜尋和探索頁使用的 AniList 動畫資料源。"
|
||||
},
|
||||
"DiscordModule": {
|
||||
"name": "Discord"
|
||||
"name": "Discord",
|
||||
"description": "將系統事件和任務結果傳送到 Discord。"
|
||||
},
|
||||
"DoubanModule": {
|
||||
"name": "豆瓣"
|
||||
"name": "豆瓣",
|
||||
"description": "電影、電視劇和音樂識別、搜尋及探索榜單使用的豆瓣資料源。"
|
||||
},
|
||||
"EmbyModule": {
|
||||
"name": "Emby"
|
||||
"name": "Emby",
|
||||
"description": "媒體伺服器適配器:同步 Emby 媒體庫並執行重新整理、播放等操作。"
|
||||
},
|
||||
"FanartModule": {
|
||||
"name": "Fanart"
|
||||
"name": "Fanart",
|
||||
"description": "媒體刮削時補充 Fanart.tv 海報、背景和藝術圖。"
|
||||
},
|
||||
"FeishuModule": {
|
||||
"name": "飛書"
|
||||
"name": "飛書",
|
||||
"description": "將系統事件和任務結果傳送到飛書機器人。"
|
||||
},
|
||||
"FileManagerModule": {
|
||||
"name": "檔案整理"
|
||||
"name": "檔案整理",
|
||||
"description": "媒體整理流程實際執行檔案讀寫、複製、移動、重新命名和刪除。"
|
||||
},
|
||||
"FilterModule": {
|
||||
"name": "過濾器"
|
||||
"name": "過濾器",
|
||||
"description": "在搜尋、下載和整理流程中執行過濾規則、識別詞和發布組比對。"
|
||||
},
|
||||
"ImdbModule": {
|
||||
"name": "IMDb"
|
||||
"name": "IMDb",
|
||||
"description": "電影和電視劇識別、搜尋使用的 IMDb 資料源。"
|
||||
},
|
||||
"IndexerModule": {
|
||||
"name": "站點索引"
|
||||
"name": "站點索引",
|
||||
"description": "負責站點索引和資源搜尋,向下載流程輸出站點資源。"
|
||||
},
|
||||
"ListenBrainzModule": {
|
||||
"name": "ListenBrainz",
|
||||
"description": "為音樂推薦頁提供 ListenBrainz 榜單和推薦資料。"
|
||||
},
|
||||
"JellyfinModule": {
|
||||
"name": "Jellyfin"
|
||||
"name": "Jellyfin",
|
||||
"description": "媒體伺服器適配器:同步 Jellyfin 媒體庫並執行重新整理、播放等操作。"
|
||||
},
|
||||
"MediaVaultModule": {
|
||||
"name": "MediaVault"
|
||||
"name": "MediaVault",
|
||||
"description": "媒體伺服器適配器:連接 MediaVault 並同步媒體庫資訊。"
|
||||
},
|
||||
"LrclibModule": {
|
||||
"name": "LRCLIB",
|
||||
"description": "為音樂刮削提供 LRCLIB 歌詞查詢。"
|
||||
},
|
||||
"MusicBrainzModule": {
|
||||
"name": "MusicBrainz",
|
||||
"description": "音樂核心識別資料源:負責音樂搜尋、錄音、專輯和藝人識別及元資料。"
|
||||
},
|
||||
"PlexModule": {
|
||||
"name": "Plex"
|
||||
"name": "Plex",
|
||||
"description": "媒體伺服器適配器:同步 Plex 媒體庫並執行重新整理、播放等操作。"
|
||||
},
|
||||
"PostgreSQLModule": {
|
||||
"name": "PostgreSQL"
|
||||
"name": "PostgreSQL",
|
||||
"description": "保存系統設定、媒體、訂閱和任務等應用程式持久化資料。"
|
||||
},
|
||||
"QbittorrentModule": {
|
||||
"name": "Qbittorrent"
|
||||
"name": "Qbittorrent",
|
||||
"description": "下載器適配器:連接 qBittorrent 並建立、暫停和管理下載任務。"
|
||||
},
|
||||
"QQBotModule": {
|
||||
"name": "QQ"
|
||||
"name": "QQ",
|
||||
"description": "將系統事件和任務結果傳送到 QQ 機器人。"
|
||||
},
|
||||
"RedisModule": {
|
||||
"name": "Redis 快取"
|
||||
"name": "Redis 快取",
|
||||
"description": "保存快取、佇列和跨程序執行狀態,支援任務排程與即時進度。"
|
||||
},
|
||||
"RtorrentModule": {
|
||||
"name": "Rtorrent"
|
||||
"name": "Rtorrent",
|
||||
"description": "下載器適配器:連接 rTorrent 並建立、暫停和管理下載任務。"
|
||||
},
|
||||
"SlackModule": {
|
||||
"name": "Slack"
|
||||
"name": "Slack",
|
||||
"description": "將系統事件和任務結果傳送到 Slack。"
|
||||
},
|
||||
"SubtitleModule": {
|
||||
"name": "站點字幕"
|
||||
"name": "站點字幕",
|
||||
"description": "從站點搜尋和下載字幕,並參與媒體整理流程。"
|
||||
},
|
||||
"SynologyChatModule": {
|
||||
"name": "Synology Chat"
|
||||
"name": "Synology Chat",
|
||||
"description": "將系統事件和任務結果傳送到 Synology Chat。"
|
||||
},
|
||||
"TelegramModule": {
|
||||
"name": "Telegram"
|
||||
"name": "Telegram",
|
||||
"description": "將系統事件和任務結果傳送到 Telegram 機器人。"
|
||||
},
|
||||
"TheMovieDbModule": {
|
||||
"name": "TheMovieDb"
|
||||
"name": "TheMovieDb",
|
||||
"description": "影視核心識別資料源:負責電影、電視劇搜尋識別,以及探索、推薦、人物和合集資料。"
|
||||
},
|
||||
"TheTvDbModule": {
|
||||
"name": "TheTvDb"
|
||||
"name": "TheTvDb",
|
||||
"description": "電視劇識別和輔助資訊使用的 TheTvDb 資料源。"
|
||||
},
|
||||
"TheAudioDbModule": {
|
||||
"name": "TheAudioDB",
|
||||
"description": "音樂識別和搜尋使用的 TheAudioDB 輔助資料源。"
|
||||
},
|
||||
"TransmissionModule": {
|
||||
"name": "Transmission"
|
||||
"name": "Transmission",
|
||||
"description": "下載器適配器:連接 Transmission 並建立、暫停和管理下載任務。"
|
||||
},
|
||||
"TrimeMediaModule": {
|
||||
"name": "飛牛影視"
|
||||
"name": "飛牛影視",
|
||||
"description": "媒體伺服器適配器:連接飛牛影視並同步媒體庫資訊。"
|
||||
},
|
||||
"UgreenModule": {
|
||||
"name": "綠聯影視"
|
||||
"name": "綠聯影視",
|
||||
"description": "媒體伺服器適配器:連接綠聯影視並同步媒體庫資訊。"
|
||||
},
|
||||
"VoceChatModule": {
|
||||
"name": "VoceChat"
|
||||
"name": "VoceChat",
|
||||
"description": "將系統事件和任務結果傳送到 VoceChat。"
|
||||
},
|
||||
"WebPushModule": {
|
||||
"name": "WebPush"
|
||||
"name": "WebPush",
|
||||
"description": "透過瀏覽器 Web Push 向使用者推送系統訊息。"
|
||||
},
|
||||
"WechatModule": {
|
||||
"name": "企業微信"
|
||||
"name": "企業微信",
|
||||
"description": "將系統事件和任務結果傳送到企業微信機器人。"
|
||||
},
|
||||
"WechatClawBotModule": {
|
||||
"name": "微信 ClawBot"
|
||||
"name": "微信 ClawBot",
|
||||
"description": "將系統事件和任務結果傳送到微信 ClawBot。"
|
||||
},
|
||||
"ZSpaceModule": {
|
||||
"name": "極影視"
|
||||
"name": "極影視",
|
||||
"description": "媒體伺服器適配器:連接極影視並同步媒體庫資訊。"
|
||||
}
|
||||
},
|
||||
"module_test": {
|
||||
"unsupported": "模組不支援測試"
|
||||
},
|
||||
"storage": {
|
||||
"local": "本地",
|
||||
"alipan": "阿里雲盤",
|
||||
"u115": "115網盤",
|
||||
"rclone": "RClone",
|
||||
"alist": "AList",
|
||||
"alistgo": "AList",
|
||||
"smb": "SMB網路共享"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -325,7 +325,8 @@ class FanartModule(_ModuleBase):
|
||||
return False, "fanart网络连接失败"
|
||||
|
||||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||||
return "FANART_API_KEY", True
|
||||
"""由 Fanart 总开关控制模块是否参与宿主运行时。"""
|
||||
return "FANART_ENABLE", True
|
||||
|
||||
@staticmethod
|
||||
def get_name() -> str:
|
||||
|
||||
@@ -12,8 +12,8 @@ priority = 0
|
||||
|
||||
[activation]
|
||||
policy = "when_configured"
|
||||
watch = ["FANART_API_KEY", "PROXY_HOST"]
|
||||
watch = ["FANART_ENABLE", "FANART_API_KEY", "PROXY_HOST"]
|
||||
|
||||
[activation.selector]
|
||||
kind = "setting_truthy"
|
||||
key = "FANART_API_KEY"
|
||||
key = "FANART_ENABLE"
|
||||
|
||||
@@ -366,6 +366,8 @@ class ConfigModel(BaseModel):
|
||||
DOH_RESOLVERS: str = "1.0.0.1,1.1.1.1,9.9.9.9,149.112.112.112"
|
||||
|
||||
# ==================== 媒体元数据配置 ====================
|
||||
# 无独立配置项的内置模块开关;未列出的模块默认开启。
|
||||
MODULE_ENABLE: Dict[str, bool] = Field(default_factory=dict)
|
||||
# 媒体搜索来源 themoviedb/douban/bangumi/anilist/imdb/musicbrainz/theaudiodb/doubanmusic,多个用,分隔
|
||||
SEARCH_SOURCE: str = "themoviedb"
|
||||
# 媒体识别来源 themoviedb/douban/bangumi/anilist/imdb/musicbrainz/theaudiodb/doubanmusic
|
||||
|
||||
@@ -13,21 +13,33 @@ from app.runtime.capabilities.model import (
|
||||
SelectorSchema,
|
||||
)
|
||||
from app.runtime.capabilities.registry import CapabilityRegistry
|
||||
from app.runtime.settings import get_runtime_setting, has_runtime_setting
|
||||
from app.runtime.extensions.service import ServiceConfigHelper
|
||||
from app.runtime.settings import get_runtime_setting, has_runtime_setting
|
||||
from app.schemas.types import (
|
||||
DownloaderType,
|
||||
MediaRecognizeType,
|
||||
MediaServerType,
|
||||
NotificationChannel,
|
||||
ModuleType,
|
||||
NotificationChannel,
|
||||
OtherModulesType,
|
||||
StorageSchema,
|
||||
SystemConfigKey,
|
||||
)
|
||||
|
||||
|
||||
HOST_MODULE_KIND = "host_module"
|
||||
MODULE_ENABLE_SETTING = "MODULE_ENABLE"
|
||||
NON_SWITCHABLE_HOST_MODULE_IDS = frozenset(
|
||||
{
|
||||
"FileManagerModule",
|
||||
"FilterModule",
|
||||
"IndexerModule",
|
||||
"MusicBrainzModule",
|
||||
"PostgreSQLModule",
|
||||
"RedisModule",
|
||||
"SubtitleModule",
|
||||
"TheMovieDbModule",
|
||||
}
|
||||
)
|
||||
_SETTING_SELECTOR = "setting_truthy"
|
||||
_SERVICE_SELECTOR = "system_config_item"
|
||||
_MODULE_ROOT = Path(__file__).resolve().parents[3] / "modules"
|
||||
@@ -163,6 +175,8 @@ def capture_host_module_config(
|
||||
"""对本轮涉及的设置和服务配置各读取一次并冻结容器。"""
|
||||
setting_keys: set[str] = set()
|
||||
service_keys: set[str] = set()
|
||||
if any(spec.activation is ActivationPolicy.BOOTSTRAP for spec in specs):
|
||||
setting_keys.add(MODULE_ENABLE_SETTING)
|
||||
for spec in specs:
|
||||
selector = spec.selector
|
||||
if selector is None:
|
||||
@@ -193,7 +207,12 @@ def should_run_host_module(
|
||||
) -> bool:
|
||||
"""依据有限 selector 语法判断能力是否应拥有运行资源。"""
|
||||
if spec.activation is ActivationPolicy.BOOTSTRAP:
|
||||
return True
|
||||
if spec.id in NON_SWITCHABLE_HOST_MODULE_IDS:
|
||||
return True
|
||||
module_settings = snapshot.settings.get(MODULE_ENABLE_SETTING, {})
|
||||
if not isinstance(module_settings, Mapping):
|
||||
return True
|
||||
return module_settings.get(spec.id, True) is not False
|
||||
if spec.activation is ActivationPolicy.ON_FIRST_USE:
|
||||
return False
|
||||
selector = spec.selector
|
||||
@@ -211,6 +230,31 @@ def should_run_host_module(
|
||||
raise ValueError(f"未支持的 Host Module selector:{selector.kind}")
|
||||
|
||||
|
||||
def should_expose_host_module_option(
|
||||
spec: CapabilitySpec,
|
||||
snapshot: HostModuleConfigSnapshot,
|
||||
) -> bool:
|
||||
"""判断模块是否应作为前端服务类型选项暴露。
|
||||
|
||||
服务模块在尚未创建任何实例时仍需展示,才能允许用户新增首个实例;
|
||||
已存在的服务配置全部关闭时则隐藏该类型,保持关闭模块后的选择器语义。
|
||||
"""
|
||||
if spec.activation is not ActivationPolicy.WHEN_CONFIGURED:
|
||||
return should_run_host_module(spec, snapshot)
|
||||
selector = spec.selector
|
||||
if selector is None or selector.kind != _SERVICE_SELECTOR:
|
||||
return should_run_host_module(spec, snapshot)
|
||||
config = selector.config
|
||||
matching = [
|
||||
item
|
||||
for item in snapshot.services[config["key"]]
|
||||
if getattr(item, config["match_field"]) == config["match_value"]
|
||||
]
|
||||
return not matching or any(
|
||||
bool(getattr(item, config["enabled_field"])) for item in matching
|
||||
)
|
||||
|
||||
|
||||
class HostModuleAdapter:
|
||||
"""把现有模块类接入 Capability Runtime,保持类路径与对象 identity 不变。"""
|
||||
|
||||
|
||||
@@ -7,28 +7,31 @@ from typing import Any, Generator, List, Optional, Tuple, Union
|
||||
from app.foundation.reflection import ObjectUtils
|
||||
from app.foundation.singleton import Singleton
|
||||
from app.runtime.capabilities.model import (
|
||||
ActivationPolicy,
|
||||
CapabilityLifecycleState,
|
||||
CapabilityObservation,
|
||||
CapabilitySpec,
|
||||
)
|
||||
from app.runtime.capabilities.runtime import CapabilityRuntime
|
||||
from app.runtime.settings import get_runtime_setting
|
||||
from app.runtime.events import Event, EventHandlerBinding, eventmanager
|
||||
from app.runtime.extensions.module.adapter import (
|
||||
HOST_MODULE_KIND,
|
||||
MODULE_ENABLE_SETTING,
|
||||
NON_SWITCHABLE_HOST_MODULE_IDS,
|
||||
HostModuleAdapter,
|
||||
build_host_module_registry,
|
||||
capture_host_module_config,
|
||||
should_run_host_module,
|
||||
)
|
||||
from app.runtime.log import logger
|
||||
from app.runtime.settings import get_runtime_setting
|
||||
from app.schemas.types import (
|
||||
DownloaderType,
|
||||
EventType,
|
||||
MediaRecognizeType,
|
||||
MediaServerType,
|
||||
NotificationChannel,
|
||||
ModuleType,
|
||||
NotificationChannel,
|
||||
OtherModulesType,
|
||||
StorageSchema,
|
||||
)
|
||||
@@ -170,7 +173,14 @@ class ModuleManager(metaclass=Singleton):
|
||||
selected = tuple(
|
||||
spec
|
||||
for spec in self._specs
|
||||
if changed_keys is None or changed_keys.intersection(spec.watch)
|
||||
if (
|
||||
changed_keys is None
|
||||
or changed_keys.intersection(spec.watch)
|
||||
or (
|
||||
spec.activation is ActivationPolicy.BOOTSTRAP
|
||||
and MODULE_ENABLE_SETTING in changed_keys
|
||||
)
|
||||
)
|
||||
)
|
||||
snapshot = capture_host_module_config(selected)
|
||||
for spec in selected:
|
||||
@@ -340,6 +350,28 @@ class ModuleManager(metaclass=Singleton):
|
||||
"""返回全部轻量模块声明,包含物化或启动失败的能力。"""
|
||||
return self._specs
|
||||
|
||||
def list_switchable_specs(self) -> tuple[CapabilitySpec, ...]:
|
||||
"""返回可由用户统一开关的内置媒体模块声明,排除核心模块及已有独立开关的模块。"""
|
||||
return tuple(
|
||||
spec
|
||||
for spec in self._specs
|
||||
if spec.activation is ActivationPolicy.BOOTSTRAP and spec.id not in NON_SWITCHABLE_HOST_MODULE_IDS
|
||||
)
|
||||
|
||||
def list_enabled_specs(self) -> tuple[CapabilitySpec, ...]:
|
||||
"""
|
||||
返回当前配置下应对外展示的模块声明
|
||||
|
||||
该视图按 selector 重新计算“应启用”状态,因此既不会展示未配置的服务模块,
|
||||
也不会因为模块启动失败而把应启用的模块从健康检查中隐藏。
|
||||
"""
|
||||
snapshot = capture_host_module_config(self._specs)
|
||||
return tuple(
|
||||
spec
|
||||
for spec in self._specs
|
||||
if should_run_host_module(spec, snapshot)
|
||||
)
|
||||
|
||||
def get_specs(self) -> tuple[CapabilitySpec, ...]:
|
||||
"""兼容内部调用命名,返回与 `list_specs` 相同的声明快照。"""
|
||||
return self.list_specs()
|
||||
|
||||
@@ -31,3 +31,13 @@ class StorageOption(BaseModel): # type: ignore[misc]
|
||||
|
||||
name: str = Field(description="存储显示名称")
|
||||
type: str = Field(description="存储类型标识")
|
||||
|
||||
|
||||
class StorageCatalogOption(BaseModel):
|
||||
"""前端新增存储和路径选择器可消费的存储类型目录项。"""
|
||||
|
||||
type: str = Field(description="存储类型标识")
|
||||
name: str = Field(description="存储原始显示名称")
|
||||
name_i18n: str = Field(description="按请求语言返回的显示名称")
|
||||
icon: str = Field(description="Vuetify 存储图标")
|
||||
remote: bool = Field(description="是否为远程存储")
|
||||
|
||||
@@ -314,7 +314,7 @@ class NetTestTarget(BaseModel):
|
||||
|
||||
|
||||
class SystemModuleInfo(BaseModel):
|
||||
"""已加载系统模块摘要。"""
|
||||
"""当前配置下已启用系统模块摘要。"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
@@ -323,11 +323,51 @@ class SystemModuleInfo(BaseModel):
|
||||
|
||||
|
||||
class SystemModuleListData(BaseModel):
|
||||
"""已加载系统模块列表。"""
|
||||
"""当前配置下已启用系统模块列表。"""
|
||||
|
||||
modules: list[SystemModuleInfo] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SystemModuleSettingInfo(BaseModel):
|
||||
"""可由用户统一开关的内置模块设置摘要。"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
name_i18n: str
|
||||
name_key: str
|
||||
description_i18n: str
|
||||
description_key: str
|
||||
enabled: bool
|
||||
|
||||
|
||||
class SystemModuleSettingListData(BaseModel):
|
||||
"""可由用户统一开关的内置模块设置列表。"""
|
||||
|
||||
modules: list[SystemModuleSettingInfo] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SystemModuleCatalogInfo(BaseModel):
|
||||
"""前端服务选择器可消费的宿主模块目录项。"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
name_i18n: str
|
||||
name_key: str
|
||||
description_i18n: str
|
||||
description_key: str
|
||||
type: str
|
||||
subtype: str
|
||||
option_value: Optional[str] = None
|
||||
enabled: bool = True
|
||||
active: bool = False
|
||||
|
||||
|
||||
class SystemModuleCatalogListData(BaseModel):
|
||||
"""宿主模块及其服务类型目录。"""
|
||||
|
||||
modules: list[SystemModuleCatalogInfo] = Field(default_factory=list)
|
||||
|
||||
|
||||
class DatabaseBackupArtifactData(BaseModel): # type: ignore[misc]
|
||||
"""Web 管理端可见的受管数据库备份摘要。"""
|
||||
|
||||
|
||||
@@ -56,25 +56,25 @@ MCP 当前不会主动发送工具列表变更通知(`listChanged=false`)。
|
||||
|
||||
`app/agent/policy/resources/api_mcp_schema.json` 是 `moviepilot_api` 的生成制品,不是设置项或 API 参数的手工事实源。`scripts/generate_agent_api_mcp_schema.py` 从当前 FastAPI OpenAPI、固定 operation 路由和 Agent 专用英文参数说明生成该文件;运行时直接读取它响应外部 MCP `tools/list`,测试会校验生成结果没有漂移。修改 API、请求模型或 operation 后应重新生成并提交该文件,不应直接编辑 JSON。
|
||||
|
||||
当前完整 FastAPI OpenAPI 包含 397 个 HTTP 操作,其中 218 个稳定业务操作进入
|
||||
`moviepilot_api`,使用 216 个固定路由模板:215 条 OpenAPI 路由直接匹配,另有 1 条只允许
|
||||
当前完整 FastAPI OpenAPI 包含 400 个 HTTP 操作,其中 220 个稳定业务操作进入
|
||||
`moviepilot_api`,使用 218 个固定路由模板:217 条 OpenAPI 路由直接匹配,另有 1 条只允许
|
||||
`tmdb`、`douban`、`bangumi`、`anilist` 四个来源的受限人物作品动态路由。每个 operation
|
||||
均同时具备固定 method/path、角色权限、副作用等级、确认与恢复策略、结果敏感性、英文用途说明,
|
||||
以及可直接提交的 path/query/body JSON Schema;Skill front matter、正文 operation 章节、运行时
|
||||
注册表和 MCP `tools/list` 的 218 个 `oneOf` 分支必须完全一致。
|
||||
注册表和 MCP `tools/list` 的 220 个 `oneOf` 分支必须完全一致。
|
||||
|
||||
数量不相等是明确的安全与语义边界,而不是漏生成。当前 397 条路由均被审计并锁定为以下一种
|
||||
数量不相等是明确的安全与语义边界,而不是漏生成。当前 400 条路由均被审计并锁定为以下一种
|
||||
归属,审计生成器不再提供“未归类”兜底:
|
||||
|
||||
| 归属 | 数量 | Agent 使用方式 |
|
||||
| :--- | ---: | :--- |
|
||||
| `gateway` | 215 | 通过 `moviepilot_api` 的稳定 operation 和精确参数合同调用 |
|
||||
| `gateway` | 217 | 通过 `moviepilot_api` 的稳定 operation 和精确参数合同调用 |
|
||||
| `consolidated` | 71 | 通过同领域聚合 operation 调用,不复制数据源或前端专用路由 |
|
||||
| `provider-skill` | 12 | 通过下载器或媒体服务器 Skill 调用第三方 provider API |
|
||||
| `alternate-auth-duplicate` | 11 | 使用对应 bearer-authenticated gateway operation,不暴露 API_TOKEN 兼容副本 |
|
||||
| `transport_or_identity` | 66 | 由登录、令牌、MCP、会话、回调、健康检查等宿主传输/身份边界拥有 |
|
||||
| `stream_or_binary` | 10 | 由直接客户端处理流式日志、消息、文件、图片等非结构化响应 |
|
||||
| `ui_presentation` | 12 | 由前端或插件渲染面拥有,不作为业务 Agent operation |
|
||||
| `ui_presentation` | 13 | 由前端或插件渲染面拥有,不作为业务 Agent operation |
|
||||
|
||||
逐路由归属见 `docs/refactor/agent-api-surface-audit.md`,并由
|
||||
`tests/test_agent_api_surface_audit.py` 对当前 OpenAPI、固定注册表、MCP schema、英文 Skill
|
||||
@@ -442,7 +442,9 @@ AMLL 使用无需鉴权的原生搜索与获取接口,先尝试 ISRC,再核
|
||||
| GET | `/api/v1/dashboard/schedule2/{job_id}/progress` | 使用 API_TOKEN 查询指定后台定时服务的实时进度详情 |
|
||||
| GET | `/api/v1/system/setting/public/{key}` | 登录用户读取白名单内非敏感系统设置,仅支持目录、存储、站点范围、默认订阅规则、Follow 订阅者和插件市场地址等前端必需配置 |
|
||||
| POST | `/api/v1/system/setting/PLUGIN_MARKET/sync-wiki` | 管理员从 MoviePilot Wiki 的插件文档同步公开插件仓库清单,和本地 `PLUGIN_MARKET` 合并去重后写入配置 |
|
||||
| GET | `/api/v1/system/modulelist` | 查询已加载模块,保留 `name` 原始中文字段,并提供 `name_i18n` 和 `name_key` 给多语言前端展示 |
|
||||
| GET | `/api/v1/system/module-catalog` | 查询宿主模块及其服务类型目录,供前端选择器构造选项 |
|
||||
| GET | `/api/v1/system/modulelist` | 查询已启用模块,保留 `name` 原始中文字段,并提供 `name_i18n` 和 `name_key` 给多语言前端展示 |
|
||||
| GET | `/api/v1/system/module-settings` | 管理员查询没有其它激活配置、可由用户统一开关的内置模块 |
|
||||
| GET | `/api/v1/system/moduletest/{moduleid}` | 测试指定模块可用性,标准响应的 `message` 会按请求语言直接返回翻译文本 |
|
||||
| GET | `/api/v1/message/agent/mcp/servers` | 管理员查询 Agent 外部 MCP 服务器配置 |
|
||||
| POST | `/api/v1/message/agent/mcp/servers` | 管理员保存 Agent 外部 MCP 服务器配置 |
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
"disposition_counts": {
|
||||
"alternate-auth-duplicate": 11,
|
||||
"consolidated": 71,
|
||||
"gateway": 215,
|
||||
"gateway": 217,
|
||||
"provider-skill": 12,
|
||||
"stream_or_binary": 10,
|
||||
"transport_or_identity": 66,
|
||||
"ui_presentation": 12
|
||||
"ui_presentation": 13
|
||||
},
|
||||
"dynamic_gateway_routes": [
|
||||
{
|
||||
@@ -18,10 +18,10 @@
|
||||
"reason": "The executor validates and expands this bounded source placeholder to one of tmdb, douban, bangumi, or anilist before calling the corresponding concrete OpenAPI route."
|
||||
}
|
||||
],
|
||||
"gateway_http_route_count": 216,
|
||||
"gateway_operation_count": 218,
|
||||
"matched_gateway_http_route_count": 215,
|
||||
"openapi_operation_count": 397,
|
||||
"gateway_http_route_count": 218,
|
||||
"gateway_operation_count": 220,
|
||||
"matched_gateway_http_route_count": 217,
|
||||
"openapi_operation_count": 400,
|
||||
"operations": [
|
||||
{
|
||||
"disposition": "consolidated",
|
||||
@@ -3441,6 +3441,18 @@
|
||||
"storage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"disposition": "ui_presentation",
|
||||
"method": "GET",
|
||||
"operation_ids": [],
|
||||
"owner": "host-ui",
|
||||
"path": "/api/v1/storage/catalog",
|
||||
"reason": "Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.",
|
||||
"summary": "查询存储类型目录",
|
||||
"tags": [
|
||||
"storage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"disposition": "gateway",
|
||||
"method": "POST",
|
||||
@@ -4193,6 +4205,34 @@
|
||||
"system"
|
||||
]
|
||||
},
|
||||
{
|
||||
"disposition": "gateway",
|
||||
"method": "GET",
|
||||
"operation_ids": [
|
||||
"system.module.catalog"
|
||||
],
|
||||
"owner": "moviepilot-api",
|
||||
"path": "/api/v1/system/module-catalog",
|
||||
"reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.",
|
||||
"summary": "查询宿主模块目录",
|
||||
"tags": [
|
||||
"system"
|
||||
]
|
||||
},
|
||||
{
|
||||
"disposition": "gateway",
|
||||
"method": "GET",
|
||||
"operation_ids": [
|
||||
"system.module.settings"
|
||||
],
|
||||
"owner": "moviepilot-api",
|
||||
"path": "/api/v1/system/module-settings",
|
||||
"reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.",
|
||||
"summary": "查询可手动开关的内置模块",
|
||||
"tags": [
|
||||
"system"
|
||||
]
|
||||
},
|
||||
{
|
||||
"disposition": "gateway",
|
||||
"method": "GET",
|
||||
@@ -4202,7 +4242,7 @@
|
||||
"owner": "moviepilot-api",
|
||||
"path": "/api/v1/system/modulelist",
|
||||
"reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.",
|
||||
"summary": "查询已加载的模块ID列表",
|
||||
"summary": "查询已启用的模块ID列表",
|
||||
"tags": [
|
||||
"system"
|
||||
]
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
## Result
|
||||
|
||||
- OpenAPI HTTP operations: **397**
|
||||
- Stable `moviepilot_api` operations: **218**
|
||||
- Exact HTTP routes used by the gateway: **216**
|
||||
- OpenAPI routes matched directly by the gateway: **215**
|
||||
- OpenAPI HTTP operations: **400**
|
||||
- Stable `moviepilot_api` operations: **220**
|
||||
- Exact HTTP routes used by the gateway: **218**
|
||||
- OpenAPI routes matched directly by the gateway: **217**
|
||||
- Bounded dynamic gateway routes: **1**
|
||||
- Every gateway operation has a generated English oneOf input contract in MCP `tools/list` and `skills/moviepilot-api/SKILL.md`.
|
||||
- Every non-gateway OpenAPI operation is listed below with an explicit ownership boundary; it is not silently callable through arbitrary URL/method input.
|
||||
@@ -19,11 +19,11 @@
|
||||
| :--- | ---: | :--- |
|
||||
| `alternate-auth-duplicate` | 11 | API-token compatibility duplicate of a bearer-authenticated capability. |
|
||||
| `consolidated` | 71 | Source/UI route represented by a stable aggregate Agent operation. |
|
||||
| `gateway` | 215 | Approved structured MoviePilot Agent operation. |
|
||||
| `gateway` | 217 | Approved structured MoviePilot Agent operation. |
|
||||
| `provider-skill` | 12 | Low-level downloader or media-server capability owned by a provider Skill. |
|
||||
| `stream_or_binary` | 10 | Streaming or binary response owned by a direct client transport. |
|
||||
| `transport_or_identity` | 66 | Authentication, protocol, callback, account, or conversation transport boundary. |
|
||||
| `ui_presentation` | 12 | Frontend or plugin-rendered presentation contract. |
|
||||
| `ui_presentation` | 13 | Frontend or plugin-rendered presentation contract. |
|
||||
|
||||
## Bounded Dynamic Routes
|
||||
|
||||
@@ -297,6 +297,7 @@
|
||||
| `DELETE` | `/api/v1/site/{site_id}` | site | `gateway` | site.delete | 删除站点 |
|
||||
| `GET` | `/api/v1/site/{site_id}` | site | `consolidated` | site.list | 站点详情 |
|
||||
| `POST` | `/api/v1/storage/agent/list` | storage | `gateway` | storage.list | 查询 Agent 可用目录和文件 |
|
||||
| `GET` | `/api/v1/storage/catalog` | storage | `ui_presentation` | host-ui | 查询存储类型目录 |
|
||||
| `POST` | `/api/v1/storage/delete` | storage | `gateway` | storage.delete | 删除文件或目录 |
|
||||
| `GET` | `/api/v1/storage/directories` | storage | `gateway` | storage.settings | 查询目录配置 |
|
||||
| `POST` | `/api/v1/storage/download` | storage | `stream_or_binary` | host-transport | 下载文件 |
|
||||
@@ -352,7 +353,9 @@
|
||||
| `GET` | `/api/v1/system/logging` | system | `stream_or_binary` | host-transport | 实时日志 |
|
||||
| `GET` | `/api/v1/system/logging/download/{name}` | system | `stream_or_binary` | host-transport | 下载日志 |
|
||||
| `GET` | `/api/v1/system/message` | system | `stream_or_binary` | host-transport | 实时消息 |
|
||||
| `GET` | `/api/v1/system/modulelist` | system | `gateway` | system.module.list | 查询已加载的模块ID列表 |
|
||||
| `GET` | `/api/v1/system/module-catalog` | system | `gateway` | system.module.catalog | 查询宿主模块目录 |
|
||||
| `GET` | `/api/v1/system/module-settings` | system | `gateway` | system.module.settings | 查询可手动开关的内置模块 |
|
||||
| `GET` | `/api/v1/system/modulelist` | system | `gateway` | system.module.list | 查询已启用的模块ID列表 |
|
||||
| `GET` | `/api/v1/system/moduletest/{moduleid}` | system | `gateway` | system.module.test | 模块可用性测试 |
|
||||
| `GET` | `/api/v1/system/nettest` | system | `gateway` | system.network.test | 测试网络连通性 |
|
||||
| `GET` | `/api/v1/system/nettest/targets` | system | `gateway` | system.network.targets | 获取网络测试目标 |
|
||||
|
||||
@@ -102,6 +102,7 @@ UI_PRESENTATION_PATHS = frozenset(
|
||||
"/api/v1/plugin/sidebar_nav",
|
||||
"/api/v1/rule/custom/reorder",
|
||||
"/api/v1/rule/groups/reorder",
|
||||
"/api/v1/storage/catalog",
|
||||
"/api/v1/storage/options",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -54,11 +54,12 @@ allowed-api-operations: >-
|
||||
workflow.share workflow.share.delete workflow.fork torrent.cache.get torrent.cache.delete
|
||||
torrent.cache.clear torrent.cache.refresh torrent.cache.reidentify database.backups.list
|
||||
database.backups.create database.backups.verify database.backups.delete filter.test
|
||||
system.network.targets system.network.test system.module.list system.module.test
|
||||
plugin.market.sync_wiki plugin.runtime.status plugin.history plugin.releases plugin.ratings
|
||||
plugin.rating plugin.rating.submit plugin.statistics plugin.reset plugin.clone config.user.get
|
||||
config.public.get system.usage.statistics plugin.folders.get plugin.folders.update
|
||||
plugin.folder.create plugin.folder.update plugin.folder.delete plugin.folder.plugins.update
|
||||
system.network.targets system.network.test system.module.list system.module.catalog
|
||||
system.module.settings system.module.test plugin.market.sync_wiki plugin.runtime.status
|
||||
plugin.history plugin.releases plugin.ratings plugin.rating plugin.rating.submit
|
||||
plugin.statistics plugin.reset plugin.clone config.user.get config.public.get
|
||||
system.usage.statistics plugin.folders.get plugin.folders.update plugin.folder.create
|
||||
plugin.folder.update plugin.folder.delete plugin.folder.plugins.update
|
||||
plugin.folder.plugin.assign plugin.folder.plugin.remove
|
||||
---
|
||||
|
||||
@@ -99,7 +100,7 @@ use a more specific skill or explain that the structured operation is unavailabl
|
||||
## API Category Index
|
||||
|
||||
Each category file contains the complete operation contracts for its namespace.
|
||||
The counts are a maintenance aid for the 218 currently exposed operations.
|
||||
The counts are a maintenance aid for the 220 currently exposed operations.
|
||||
|
||||
| Category | Detail file | Operation namespace | Count | Use for |
|
||||
| --- | --- | --- | ---: | --- |
|
||||
|
||||
@@ -4,16 +4,30 @@ Version, update, restart, module, network, and usage operations.
|
||||
|
||||
## Operations
|
||||
|
||||
### `system.module.catalog`
|
||||
`GET /api/v1/system/module-catalog`; policy effect: `safe_read`.
|
||||
Purpose: List MoviePilot host modules and service-type options for UI selectors.
|
||||
- `path_params`: none
|
||||
- `query`: none
|
||||
- `body`: none
|
||||
|
||||
### `system.module.list`
|
||||
`GET /api/v1/system/modulelist`; policy effect: `safe_read`.
|
||||
Purpose: List loaded MoviePilot module IDs and localized names.
|
||||
Purpose: List enabled MoviePilot module IDs and localized names.
|
||||
- `path_params`: none
|
||||
- `query`: none
|
||||
- `body`: none
|
||||
|
||||
### `system.module.settings`
|
||||
`GET /api/v1/system/module-settings`; policy effect: `safe_read`.
|
||||
Purpose: List built-in MoviePilot modules that can be enabled or disabled by an administrator.
|
||||
- `path_params`: none
|
||||
- `query`: none
|
||||
- `body`: none
|
||||
|
||||
### `system.module.test`
|
||||
`GET /api/v1/system/moduletest/{moduleid}`; policy effect: `external_side_effect`.
|
||||
Purpose: Run the built-in availability test for one loaded MoviePilot module.
|
||||
Purpose: Run the built-in availability test for one enabled MoviePilot module.
|
||||
- `path_params`: `moduleid*` (string): Exact loaded module ID returned by system.module.list.
|
||||
- `query`: none
|
||||
- `body`: none
|
||||
|
||||
@@ -16,10 +16,11 @@ import pytest
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.foundation.singleton import Singleton
|
||||
from app.runtime.capabilities.errors import CapabilityRuntimeClosedError
|
||||
from app.runtime.capabilities.model import CapabilityLifecycleState, SelectorSchema
|
||||
from app.runtime.capabilities.model import ActivationPolicy, CapabilityLifecycleState, SelectorSchema
|
||||
from app.runtime.capabilities.registry import CapabilityRegistry
|
||||
from app.runtime.events import Event, EventHandlerBinding, eventmanager
|
||||
from app.runtime.extensions.module import manager as module_manager_extension
|
||||
from app.runtime.extensions.module.adapter import HostModuleConfigSnapshot, should_run_host_module
|
||||
from app.runtime.extensions.module.manager import ModuleManager
|
||||
from app.runtime.extensions.service import configure_service_config_reader
|
||||
from app.schemas.event import ConfigChangeEventData
|
||||
@@ -351,6 +352,36 @@ def test_specs_are_lightweight_and_do_not_materialize_modules(
|
||||
assert manager.get_running_module("SampleModule") is None
|
||||
|
||||
|
||||
def test_list_enabled_specs_only_exposes_selector_enabled_modules(
|
||||
module_manager_harness,
|
||||
) -> None:
|
||||
"""健康检查模块目录只应包含当前 selector 判定为启用的声明。"""
|
||||
manager = module_manager_harness.manager
|
||||
|
||||
assert manager.list_enabled_specs() == ()
|
||||
|
||||
_enable_sample(module_manager_harness.config_values)
|
||||
|
||||
assert [spec.id for spec in manager.list_enabled_specs()] == ["SampleModule"]
|
||||
|
||||
|
||||
def test_bootstrap_module_switch_defaults_on_and_honors_explicit_disable() -> None:
|
||||
"""统一模块开关默认开启,显式 false 才会停止 Bootstrap 模块。"""
|
||||
spec = SimpleNamespace(id="BootstrapModule", activation=ActivationPolicy.BOOTSTRAP)
|
||||
enabled_snapshot = HostModuleConfigSnapshot(settings={}, services={})
|
||||
disabled_snapshot = HostModuleConfigSnapshot(
|
||||
settings={"MODULE_ENABLE": {"BootstrapModule": False}},
|
||||
services={},
|
||||
)
|
||||
|
||||
assert should_run_host_module(spec, enabled_snapshot) is True
|
||||
assert should_run_host_module(spec, disabled_snapshot) is False
|
||||
|
||||
for module_id in ("FileManagerModule", "MusicBrainzModule", "SubtitleModule"):
|
||||
protected_spec = SimpleNamespace(id=module_id, activation=ActivationPolicy.BOOTSTRAP)
|
||||
assert should_run_host_module(protected_spec, disabled_snapshot) is True
|
||||
|
||||
|
||||
def test_get_module_materializes_one_canonical_class_without_starting_it(
|
||||
module_manager_harness,
|
||||
) -> None:
|
||||
@@ -892,6 +923,7 @@ def empty_config(self, key=None):
|
||||
SystemConfigOper.get = empty_config
|
||||
settings.ACOUSTID_API_KEY = None
|
||||
settings.FANART_API_KEY = None
|
||||
settings.FANART_ENABLE = False
|
||||
|
||||
from app.runtime.extensions.module.manager import ModuleManager
|
||||
from app.application.module import configure_module_runtime
|
||||
@@ -904,7 +936,19 @@ assert manager.get_specs() == manager.list_specs()
|
||||
|
||||
from app.api.endpoints.system import modulelist
|
||||
response = modulelist(None)
|
||||
assert len(response.data["modules"]) == 41
|
||||
assert len(response.data["modules"]) == 17
|
||||
|
||||
switchable_ids = {spec.id for spec in manager.list_switchable_specs()}
|
||||
assert {
|
||||
"FileManagerModule",
|
||||
"FilterModule",
|
||||
"IndexerModule",
|
||||
"MusicBrainzModule",
|
||||
"PostgreSQLModule",
|
||||
"RedisModule",
|
||||
"SubtitleModule",
|
||||
"TheMovieDbModule",
|
||||
}.isdisjoint(switchable_ids)
|
||||
|
||||
heavy_prefixes = (
|
||||
"lark_oapi",
|
||||
@@ -1008,6 +1052,7 @@ SystemConfigOper.get = lambda self, key=None: {} if key is None else []
|
||||
from app.runtime.config import settings
|
||||
settings.ACOUSTID_API_KEY = None
|
||||
settings.FANART_API_KEY = None
|
||||
settings.FANART_ENABLE = False
|
||||
|
||||
from app.runtime.extensions.module.manager import ModuleManager
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from unittest.mock import patch
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.api.endpoints import system as system_endpoint
|
||||
from app.runtime.localization import LocaleHelper
|
||||
@@ -8,8 +9,14 @@ from app.runtime.localization import LocaleHelper
|
||||
class _FakeModuleManager:
|
||||
"""提供 system 模块接口测试所需的最小模块管理器。"""
|
||||
|
||||
def list_specs(self) -> tuple:
|
||||
"""返回 manifest 元数据视图。"""
|
||||
def list_enabled_specs(self) -> tuple:
|
||||
"""返回当前配置下的 manifest 元数据视图。"""
|
||||
return (
|
||||
SimpleNamespace(id="DoubanModule", metadata={"name": "豆瓣"}),
|
||||
)
|
||||
|
||||
def list_switchable_specs(self) -> tuple:
|
||||
"""返回后端声明的可手动开关模块。"""
|
||||
return (
|
||||
SimpleNamespace(id="DoubanModule", metadata={"name": "豆瓣"}),
|
||||
)
|
||||
@@ -47,3 +54,39 @@ def test_system_moduletest_localizes_message():
|
||||
assert response.success is False
|
||||
assert response.message == "Module does not support testing"
|
||||
assert not hasattr(response, "message_i18n")
|
||||
|
||||
|
||||
def test_system_module_settings_uses_backend_catalog_and_persisted_switch():
|
||||
"""模块设置接口应返回后端目录,并按持久化开关计算状态。"""
|
||||
token = LocaleHelper.set_current_locale("en-US")
|
||||
|
||||
class _FakeRuntimeSettings:
|
||||
"""提供模块设置接口所需的最小运行配置读取器。"""
|
||||
|
||||
@staticmethod
|
||||
def get(key: str, default=None):
|
||||
"""返回测试用的模块开关映射。"""
|
||||
if key == "MODULE_ENABLE":
|
||||
return {"DoubanModule": False}
|
||||
return default
|
||||
|
||||
with (
|
||||
patch.object(system_endpoint, "get_module_manager", return_value=_FakeModuleManager()),
|
||||
patch.object(system_endpoint, "get_runtime_settings", return_value=_FakeRuntimeSettings()),
|
||||
):
|
||||
try:
|
||||
response = asyncio.run(system_endpoint.module_settings(_="admin"))
|
||||
finally:
|
||||
LocaleHelper.reset_current_locale(token)
|
||||
|
||||
assert response.data["modules"] == [
|
||||
{
|
||||
"id": "DoubanModule",
|
||||
"name": "豆瓣",
|
||||
"name_i18n": "Douban",
|
||||
"name_key": "system.modules.DoubanModule.name",
|
||||
"description_i18n": "Douban data source for movie, TV, and music identification, search, and discovery lists.",
|
||||
"description_key": "system.modules.DoubanModule.description",
|
||||
"enabled": False,
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user