From 4395ddecf2c4dd609ab1a06ac835b53acaa2b467 Mon Sep 17 00:00:00 2001 From: thsrite Date: Wed, 30 Sep 2026 19:41:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(mediaserver):=20=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E5=AA=92=E4=BD=93=E6=9C=8D=E5=8A=A1=20MediaVault=20=E6=9B=B4?= =?UTF-8?q?=E5=90=8D=E4=B8=BA=20Vyo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主项目更名为 Muvyo,内置媒体服务更名为 Vyo。模块目录、文件、类名、模块 ID、枚举、 名称描述、日志与文档统一改为 Vyo;配置类型值 mediavault 保持不变, 已保存的媒体服务器配置无需迁移。 --- app/agent/tools/impl/service.py | 2 +- app/locales/en-US.json | 6 +- app/locales/zh-CN.json | 6 +- app/locales/zh-TW.json | 6 +- app/modules/{mediavault => vyo}/__init__.py | 8 +- app/modules/{mediavault => vyo}/api.py | 22 +++--- .../{mediavault => vyo}/capability.toml | 8 +- app/modules/{mediavault => vyo}/module.py | 43 +++++------ .../{mediavault/mediavault.py => vyo/vyo.py} | 28 +++---- app/runtime/extensions/module/quality.py | 2 +- app/schemas/types.py | 4 +- docs/mcp-api.md | 2 +- .../scripts/mp-mediaserver.py | 2 +- .../architecture/dependency-baseline.json | 74 +++++++++---------- ...ediavault_module.py => test_vyo_module.py} | 32 ++++---- 15 files changed, 123 insertions(+), 122 deletions(-) rename app/modules/{mediavault => vyo}/__init__.py (77%) rename app/modules/{mediavault => vyo}/api.py (83%) rename app/modules/{mediavault => vyo}/capability.toml (72%) rename app/modules/{mediavault => vyo}/module.py (83%) rename app/modules/{mediavault/mediavault.py => vyo/vyo.py} (95%) rename tests/{test_mediavault_module.py => test_vyo_module.py} (96%) diff --git a/app/agent/tools/impl/service.py b/app/agent/tools/impl/service.py index d5c17fc4d..1cf403bd6 100644 --- a/app/agent/tools/impl/service.py +++ b/app/agent/tools/impl/service.py @@ -413,7 +413,7 @@ class MediaServerOperationTool(_ServiceOperationTool): name: str = "mediaserver_operation" description: str = ( "Operate a configured Emby, Jellyfin, Plex, ZSpace, UGREEN, TrimeMedia, " - "Navidrome, or MediaVault server. The input schema contains one exact branch per action, " + "Navidrome, or Vyo server. The input schema contains one exact branch per action, " "including providers, effects, required fields, types, defaults, enums, nested " "item fields, and cross-field constraints." ) diff --git a/app/locales/en-US.json b/app/locales/en-US.json index e52ef3241..ff50f4fcc 100644 --- a/app/locales/en-US.json +++ b/app/locales/en-US.json @@ -57,9 +57,9 @@ "name": "Jellyfin", "description": "Media-server adapter that synchronizes Jellyfin libraries and runs refresh or playback actions." }, - "MediaVaultModule": { - "name": "MediaVault", - "description": "Media-server adapter that connects to MediaVault and synchronizes its library." + "VyoModule": { + "name": "Vyo", + "description": "Media-server adapter that connects to Vyo (the built-in media service of Muvyo) and synchronizes its library." }, "LrclibModule": { "name": "LRCLIB", diff --git a/app/locales/zh-CN.json b/app/locales/zh-CN.json index 6f75bc7c3..5a22c23ea 100644 --- a/app/locales/zh-CN.json +++ b/app/locales/zh-CN.json @@ -57,9 +57,9 @@ "name": "Jellyfin", "description": "媒体服务器适配器:同步 Jellyfin 媒体库并执行刷新、播放等操作。" }, - "MediaVaultModule": { - "name": "MediaVault", - "description": "媒体服务器适配器:连接 MediaVault 并同步媒体库信息。" + "VyoModule": { + "name": "Vyo", + "description": "媒体服务器适配器:连接 Vyo(Muvyo 内置媒体服务)并同步媒体库信息。" }, "LrclibModule": { "name": "LRCLIB", diff --git a/app/locales/zh-TW.json b/app/locales/zh-TW.json index 927a1b239..17adf6d44 100644 --- a/app/locales/zh-TW.json +++ b/app/locales/zh-TW.json @@ -57,9 +57,9 @@ "name": "Jellyfin", "description": "媒體伺服器適配器:同步 Jellyfin 媒體庫並執行重新整理、播放等操作。" }, - "MediaVaultModule": { - "name": "MediaVault", - "description": "媒體伺服器適配器:連接 MediaVault 並同步媒體庫資訊。" + "VyoModule": { + "name": "Vyo", + "description": "媒體伺服器適配器:連接 Vyo(Muvyo 內建媒體服務)並同步媒體庫資訊。" }, "LrclibModule": { "name": "LRCLIB", diff --git a/app/modules/mediavault/__init__.py b/app/modules/vyo/__init__.py similarity index 77% rename from app/modules/mediavault/__init__.py rename to app/modules/vyo/__init__.py index fd163595b..b3a66569f 100644 --- a/app/modules/mediavault/__init__.py +++ b/app/modules/vyo/__init__.py @@ -1,10 +1,10 @@ -"""MediaVault 宿主模块的惰性兼容入口。""" +"""Vyo 宿主模块的惰性兼容入口。""" from importlib import import_module from typing import Any _EXPORTS = { - "MediaVaultModule": ("app.modules.mediavault.module", "MediaVaultModule"), + "VyoModule": ("app.modules.vyo.module", "VyoModule"), } @@ -15,7 +15,7 @@ def __getattr__(name: str) -> Any: raise AttributeError(f"module {__name__!r} has no attribute {name!r}") module_name, symbol_name = contract value = getattr(import_module(module_name), symbol_name) - if name == "MediaVaultModule": + if name == "VyoModule": value.__module__ = __name__ globals()[name] = value return value @@ -26,4 +26,4 @@ def __dir__() -> list[str]: return sorted({*globals(), *_EXPORTS}) -__all__ = ["MediaVaultModule"] +__all__ = ["VyoModule"] diff --git a/app/modules/mediavault/api.py b/app/modules/vyo/api.py similarity index 83% rename from app/modules/mediavault/api.py rename to app/modules/vyo/api.py index 99d11f94d..e75e679c1 100644 --- a/app/modules/mediavault/api.py +++ b/app/modules/vyo/api.py @@ -9,7 +9,7 @@ from app.runtime.settings import get_runtime_setting @dataclass class Result: - """MediaVault 统一响应包装。""" + """Vyo 统一响应包装。""" success: bool data: Optional[Union[Dict[str, Any], List[Any], str, int, bool]] = None @@ -18,10 +18,10 @@ class Result: class Api: - """MediaVault 自建媒体库的原生 HTTP 接口。 + """Vyo 媒体服务的原生 HTTP 接口。 统一走管理端口的 `/api/v1`,凭据为管理面板的长效 API Key; - 自建媒体库的接口都挂在 `/api/v1/media-library` 之下。 + Vyo 的接口都挂在 `/api/v1/media-library` 之下。 """ LIBRARY_PATH = "/api/v1/media-library" @@ -46,7 +46,7 @@ class Api: def image_url(self, item_id: str, image_type: str, host: Optional[str] = None) -> str: """拼装图片直链;item_id 传媒体库 ID 时得到媒体库封面。 - MediaVault 的图片是免鉴权资源,这里**不能**带上 API Key:该地址会随接口 + Vyo 的图片是免鉴权资源,这里**不能**带上 API Key:该地址会随接口 响应交给浏览器加载,带 Key 等于把管理凭据下发到客户端。 """ if not self.configured or not item_id: @@ -63,9 +63,9 @@ class Api: base_path: Optional[str] = None, suppress_log: bool = False, ) -> Optional[Result]: - """请求 MediaVault 接口。 + """请求 Vyo 接口。 - :param api: 接口路径,默认相对自建媒体库前缀 + :param api: 接口路径,默认相对 Vyo 接口前缀 :param base_path: 覆盖接口前缀(如站点级 `/api/v1`) :param suppress_log: 探测类调用可关闭错误日志 :return: 网络层失败返回 None,业务失败返回 success=False 的 Result @@ -88,28 +88,28 @@ class Api: ) except Exception as err: if not suppress_log: - logger.error(f"请求 MediaVault 接口 {url} 异常:{err}") + logger.error(f"请求 Vyo 接口 {url} 异常:{err}") return None if res is None: if not suppress_log: - logger.error(f"请求 MediaVault 接口 {url} 无响应") + logger.error(f"请求 Vyo 接口 {url} 无响应") return None if res.status_code >= 400: message = self.__error_message(res) if not suppress_log: - logger.error(f"请求 MediaVault 接口 {url} 失败:{res.status_code} {message}") + logger.error(f"请求 Vyo 接口 {url} 失败:{res.status_code} {message}") return Result(False, None, message, res.status_code) try: body = res.json() except Exception as err: if not suppress_log: - logger.error(f"解析 MediaVault 接口 {url} 响应失败:{err}") + logger.error(f"解析 Vyo 接口 {url} 响应失败:{err}") return None if isinstance(body, dict) and "success" in body: if not body.get("success"): message = str(body.get("message") or body.get("detail") or "") if not suppress_log: - logger.error(f"请求 MediaVault 接口 {url} 未成功:{message}") + logger.error(f"请求 Vyo 接口 {url} 未成功:{message}") return Result(False, None, message, res.status_code) return Result(True, body.get("data"), None, res.status_code) return Result(True, body, None, res.status_code) diff --git a/app/modules/mediavault/capability.toml b/app/modules/vyo/capability.toml similarity index 72% rename from app/modules/mediavault/capability.toml rename to app/modules/vyo/capability.toml index 70c517782..2d37c1876 100644 --- a/app/modules/mediavault/capability.toml +++ b/app/modules/vyo/capability.toml @@ -1,13 +1,13 @@ schema_version = 1 -id = "MediaVaultModule" +id = "VyoModule" kind = "host_module" -entrypoint = "app.modules.mediavault:MediaVaultModule" +entrypoint = "app.modules.vyo:VyoModule" depends_on = [] [metadata] -name = "MediaVault" +name = "Vyo" type = "mediaserver" -subtype = "MediaVault" +subtype = "Vyo" priority = 7 [activation] diff --git a/app/modules/mediavault/module.py b/app/modules/vyo/module.py similarity index 83% rename from app/modules/mediavault/module.py rename to app/modules/vyo/module.py index 010f0e932..e84cb67d8 100644 --- a/app/modules/mediavault/module.py +++ b/app/modules/vyo/module.py @@ -1,7 +1,7 @@ from typing import Any, Dict, Generator, List, Optional, Tuple, Union from app.modules._base.mediaserver import _MediaServerModuleBase -from app.modules.mediavault.mediavault import MediaVault +from app.modules.vyo.vyo import Vyo from app.runtime.log import logger from app.schemas.dashboard import Statistic as _SchemaStatistic from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem @@ -11,10 +11,11 @@ from app.schemas.mediaserver import MediaServerSeasonInfo as _SchemaMediaServerS from app.schemas.types import MediaServerType, ModuleType -class MediaVaultModule(_MediaServerModuleBase[MediaVault]): - """MediaVault 自建媒体库模块。""" +class VyoModule(_MediaServerModuleBase[Vyo]): + """Vyo 媒体服务模块。""" # 媒体库标识(ExistMediaInfo.server_type) + # 配置类型值沿用 mediavault,兼容已保存的媒体服务器配置 _server_type_value = "mediavault" def init_module(self) -> None: @@ -22,15 +23,15 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): 初始化模块 """ super().init_service( - service_name=MediaVault.__name__.lower(), - service_type=lambda conf: MediaVault( + service_name=Vyo.__name__.lower(), + service_type=lambda conf: Vyo( **conf.config, sync_libraries=conf.sync_libraries ), ) @staticmethod def get_name() -> str: - return "MediaVault" + return "Vyo" @staticmethod def get_type() -> ModuleType: @@ -44,7 +45,7 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): """ 获取模块子类型 """ - return MediaServerType.MediaVault + return MediaServerType.Vyo @staticmethod def get_priority() -> int: @@ -57,7 +58,7 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): """本模块不使用开关设置。""" return None - def _is_inactive(self, server: MediaVault) -> bool: + def _is_inactive(self, server: Vyo) -> bool: """未配置的实例不参与定时重连。""" return server.is_configured() and server.is_inactive() @@ -67,9 +68,9 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): try: server.disconnect() except Exception as err: - logger.error(f"停止 MediaVault 模块实例失败:{err}") + logger.error(f"停止 Vyo 模块实例失败:{err}") - def _test_server(self, server: MediaVault, name: str) -> Optional[str]: + def _test_server(self, server: Vyo, name: str) -> Optional[str]: """用配置完整性与 API Key 探测结果判断连接状态。""" if not server.is_configured(): return f"{self.get_name()}配置不完整:{name}" @@ -84,7 +85,7 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): 媒体数量统计 """ if server: - server_obj: Optional[MediaVault] = self.get_instance(server) + server_obj: Optional[Vyo] = self.get_instance(server) servers = [server_obj] if server_obj else [] else: servers = list(self.get_instances().values()) @@ -103,7 +104,7 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): """ 媒体库列表 """ - server_obj: Optional[MediaVault] = self.get_instance(server) + server_obj: Optional[Vyo] = self.get_instance(server) if server_obj: return server_obj.get_librarys(hidden=hidden) return None @@ -123,7 +124,7 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): :param start_index: 起始索引 :param limit: 每次请求的最大项目数,None 或 -1 表示一次性获取所有数据 """ - server_obj: Optional[MediaVault] = self.get_instance(server) + server_obj: Optional[Vyo] = self.get_instance(server) if server_obj: return server_obj.get_items(library_id, start_index, limit) return None @@ -134,7 +135,7 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): """ 获取指定媒体库可同步的媒体条目总数 """ - server_obj: Optional[MediaVault] = self.get_instance(server) + server_obj: Optional[Vyo] = self.get_instance(server) if server_obj: return server_obj.get_items_count(library_id) return None @@ -145,7 +146,7 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): """ 媒体库项目详情 """ - server_obj: Optional[MediaVault] = self.get_instance(server) + server_obj: Optional[Vyo] = self.get_instance(server) if server_obj: return server_obj.get_iteminfo(str(item_id)) return None @@ -156,7 +157,7 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): """ 获取剧集信息 """ - server_obj: Optional[MediaVault] = self.get_instance(server) + server_obj: Optional[Vyo] = self.get_instance(server) if not server_obj: return None _, seasoninfo = server_obj.get_tv_episodes(item_id=str(item_id)) @@ -173,7 +174,7 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): """ 获取指定季的集号到条目 ID 映射 """ - server_obj: Optional[MediaVault] = self.get_instance(server) + server_obj: Optional[Vyo] = self.get_instance(server) if not server_obj: return None return server_obj.get_season_episode_ids(str(item_id), season) @@ -184,7 +185,7 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): """ 获取媒体服务器正在播放信息 """ - server_obj: Optional[MediaVault] = self.get_instance(server) + server_obj: Optional[Vyo] = self.get_instance(server) if not server_obj: return None return server_obj.get_resume(num=count) @@ -195,7 +196,7 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): """ 获取媒体库播放地址 """ - server_obj: Optional[MediaVault] = self.get_instance(server) + server_obj: Optional[Vyo] = self.get_instance(server) if not server_obj: return None return server_obj.get_play_url(str(item_id)) @@ -206,7 +207,7 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): """ 获取媒体服务器最新入库条目 """ - server_obj: Optional[MediaVault] = self.get_instance(server) + server_obj: Optional[Vyo] = self.get_instance(server) if not server_obj: return None return server_obj.get_latest(num=count) @@ -225,7 +226,7 @@ class MediaVaultModule(_MediaServerModuleBase[MediaVault]): :param count: 获取数量 :param remote: True为外网链接,False为内网链接 """ - server_obj: Optional[MediaVault] = self.get_instance(server) + server_obj: Optional[Vyo] = self.get_instance(server) if not server_obj: return [] return server_obj.get_latest_backdrops(num=count, remote=remote) or [] diff --git a/app/modules/mediavault/mediavault.py b/app/modules/vyo/vyo.py similarity index 95% rename from app/modules/mediavault/mediavault.py rename to app/modules/vyo/vyo.py index 199df08ca..5a0f72145 100644 --- a/app/modules/mediavault/mediavault.py +++ b/app/modules/vyo/vyo.py @@ -3,7 +3,7 @@ from typing import Any, Dict, Generator, List, Optional, Tuple, Union from app.application.mediaserver import MediaServerIdentityHelper from app.foundation.url import UrlUtils -from app.modules.mediavault.api import Api +from app.modules.vyo.api import Api from app.runtime.log import logger from app.schemas.dashboard import Statistic as _SchemaStatistic from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem @@ -22,14 +22,14 @@ class _SearchInterrupted(Exception): """ -class MediaVault: - """MediaVault 自建媒体库客户端。 +class Vyo: + """Vyo 媒体服务客户端。 - 只使用 MediaVault 管理端口的原生接口,凭据是管理面板的长效 API Key; - 自建媒体库没有音乐库,也不主动外发 Webhook,相关能力在模块层显式留空。 + 只使用 Muvyo 管理端口的原生接口,凭据是 Muvyo 管理面板的长效 API Key; + Vyo 没有音乐库,也不主动外发 Webhook,相关能力在模块层显式留空。 """ - # MediaVault 单次列表请求的最大条数,与服务端 page_size 上限一致 + # Vyo 单次列表请求的最大条数,与服务端 page_size 上限一致 PAGE_LIMIT = 100 # 媒体库类型到 MoviePilot 媒体类型的映射 LIBRARY_TYPES = {"movies": MediaType.MOVIE.value, "tvshows": MediaType.TV.value} @@ -51,7 +51,7 @@ class MediaVault: self._api = Api(host=host, apikey=apikey) self._active = False if not self.is_configured(): - logger.error("MediaVault 配置不完整!") + logger.error("Vyo 配置不完整!") return self.reconnect() @@ -85,7 +85,7 @@ class MediaVault: self._api.close() def authenticate(self, username: str, password: str) -> Optional[str]: - """用 MediaVault 账号完成用户认证,返回访问令牌。""" + """用 Vyo 账号完成用户认证,返回访问令牌。""" if not self.is_configured() or not username or not password: return None result = self._api.request( @@ -209,7 +209,7 @@ class MediaVault: sort_by: str = "", sort_order: str = "", ) -> Optional[Dict[str, Any]]: - """调用条目列表接口;MediaVault 只接受页码,偏移量由调用方换算。""" + """调用条目列表接口;Vyo 只接受页码,偏移量由调用方换算。""" params: Dict[str, Any] = {"page": max(1, page), "page_size": page_size} if library_id: params["library_id"] = library_id @@ -233,7 +233,7 @@ class MediaVault: def __search_rows(self, keyword: str, kinds: str) -> Optional[Generator[Dict[str, Any], Any, None]]: """按关键字翻页产出条目原始记录。 - MediaVault 的关键字查询是模糊匹配,命中数可能超过单页上限;只读第一页会让 + Vyo 的关键字查询是模糊匹配,命中数可能超过单页上限;只读第一页会让 存在性判断把排在后面的目标误判成「未入库」,因此这里翻页直到取完。 连接失败返回 None,与「查得到但没有」区分开。 """ @@ -422,7 +422,7 @@ class MediaVault: page += 1 def __format_item_info(self, row: Dict[str, Any]) -> Optional[_SchemaMediaServerItem]: - """把 MediaVault 条目转换为统一媒体服务器模型。""" + """把 Vyo 条目转换为统一媒体服务器模型。""" try: metadata = row.get("metadata_info") or {} provider_ids: Dict[str, Any] = {} @@ -454,7 +454,7 @@ class MediaVault: ) if user_data else None, ) except Exception as err: - logger.error(f"解析 MediaVault 条目失败:{err}") + logger.error(f"解析 Vyo 条目失败:{err}") return None @staticmethod @@ -615,7 +615,7 @@ class MediaVault: matched.add(library_id) else: unmatched = True - logger.info(f"MediaVault 中未找到 {item.title} 对应的媒体库,将扫描全部媒体库") + logger.info(f"Vyo 中未找到 {item.title} 对应的媒体库,将扫描全部媒体库") if unmatched: return self.refresh_root_library() # 先全部发出排队请求再汇总:交给 all() 的生成器会在首个失败处短路, @@ -641,7 +641,7 @@ class MediaVault: return "" def __queue_scan(self, library_id: str) -> bool: - """把媒体库扫描排进 MediaVault 的后台队列,不阻塞入库流程。""" + """把媒体库扫描排进 Vyo 的后台队列,不阻塞入库流程。""" if not library_id: return False result = self._api.request(f"/libraries/{library_id}/scan-task", method="post") diff --git a/app/runtime/extensions/module/quality.py b/app/runtime/extensions/module/quality.py index ae8b61f79..a3a6deb76 100644 --- a/app/runtime/extensions/module/quality.py +++ b/app/runtime/extensions/module/quality.py @@ -68,7 +68,7 @@ BASELINE_ASSESSED_MODULES = frozenset( "jellyfin", "listenbrainz", "lrclib", - "mediavault", + "vyo", "musicbrainz", "navidrome", "plex", diff --git a/app/schemas/types.py b/app/schemas/types.py index 8f9d35550..31e7bb9f5 100644 --- a/app/schemas/types.py +++ b/app/schemas/types.py @@ -592,8 +592,8 @@ class MediaServerType(Enum): Ugreen = "Ugreen" # Navidrome 音乐服务器 Navidrome = "Navidrome" - # MediaVault 自建媒体库 - MediaVault = "MediaVault" + # Vyo 媒体服务 + Vyo = "Vyo" # 识别器类型 diff --git a/docs/mcp-api.md b/docs/mcp-api.md index 3df54a70b..3694c6f08 100644 --- a/docs/mcp-api.md +++ b/docs/mcp-api.md @@ -140,7 +140,7 @@ MCP 当前不会主动发送工具列表变更通知(`listChanged=false`)。 | :--- | :--- | :--- | | `moviepilot_api` | MoviePilot 产品业务 API:媒体、搜索、订阅、下载、整理、站点、存储、调度、工作流、插件、过滤规则和系统配置 | `skills/moviepilot-api/SKILL.md` 及其 `api/.md` 独立分类合同;运行时 schema 为 `app/agent/policy/resources/api_mcp_schema.json` | | `downloader_operation` | qBittorrent、Transmission、rTorrent 原生任务、队列、文件、限速、标签和会话操作 | `skills/downloader-operation/SKILL.md` 与 `skills/downloader-operation/scripts/mp-downloader.py` 的 `ACTIONS` | -| `mediaserver_operation` | Emby、Jellyfin、Plex、ZSpace、UGREEN、TrimeMedia、Navidrome、MediaVault 原生媒体库、搜索、播放、扫描和刷新操作 | `skills/mediaserver-operation/SKILL.md` 与 `skills/mediaserver-operation/scripts/mp-mediaserver.py` 的 `ACTIONS` | +| `mediaserver_operation` | Emby、Jellyfin、Plex、ZSpace、UGREEN、TrimeMedia、Navidrome、Vyo 原生媒体库、搜索、播放、扫描和刷新操作 | `skills/mediaserver-operation/SKILL.md` 与 `skills/mediaserver-operation/scripts/mp-mediaserver.py` 的 `ACTIONS` | | `database_operation` | MoviePilot 配置数据库表清单、实时 schema、只读 SQL 和明确授权写入 | `skills/database-operation/SKILL.md` 与 `skills/database-operation/scripts/mp-db.py` 的 `ACTIONS` | 这四个工具都要求管理员级 MCP 集成身份;`tools/list` 的可见性不等于绕过业务权限或写操作确认。下载器和媒体服务器工具会在一次调用内自动选择默认/唯一实例;实例不明确时,错误结果会列出可复用的精确实例名。数据库工具不接受任意连接串或凭据,脚本从 MoviePilot 运行时配置读取数据库连接。 diff --git a/skills/mediaserver-operation/scripts/mp-mediaserver.py b/skills/mediaserver-operation/scripts/mp-mediaserver.py index 227671e59..a1e38e0bb 100644 --- a/skills/mediaserver-operation/scripts/mp-mediaserver.py +++ b/skills/mediaserver-operation/scripts/mp-mediaserver.py @@ -35,7 +35,7 @@ PROVIDER_CLASSES = { "ugreen": "app.modules.ugreen.ugreen:Ugreen", "trimemedia": "app.modules.trimemedia.trimemedia:TrimeMedia", "navidrome": "app.modules.navidrome.navidrome:Navidrome", - "mediavault": "app.modules.mediavault.mediavault:MediaVault", + "mediavault": "app.modules.vyo.vyo:Vyo", } _UNSET = object() diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index d8c4b0c02..3822c33e7 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -931,7 +931,7 @@ } }, "edge_count": 9218, - "edge_sha256": "50c110ffa461d8c05104774cbd16d7d28ba0ad33fbfe16c847e61dcc4a5ff6bf", + "edge_sha256": "6b4e9129b86a6618ca5a42d4c90a8f345af18a115a894deb5ea5c4dc07be3aa3", "edges": [ "app -> app.foundation", "app -> app.foundation.environment", @@ -7561,38 +7561,6 @@ "app.modules.lrclib -> app.runtime.settings", "app.modules.lrclib -> app.schemas", "app.modules.lrclib -> app.schemas.types", - "app.modules.mediavault.api -> app.adapters", - "app.modules.mediavault.api -> app.adapters.network", - "app.modules.mediavault.api -> app.adapters.network.http", - "app.modules.mediavault.api -> app.foundation", - "app.modules.mediavault.api -> app.foundation.url", - "app.modules.mediavault.api -> app.runtime", - "app.modules.mediavault.api -> app.runtime.log", - "app.modules.mediavault.api -> app.runtime.settings", - "app.modules.mediavault.mediavault -> app.application", - "app.modules.mediavault.mediavault -> app.application.mediaserver", - "app.modules.mediavault.mediavault -> app.foundation", - "app.modules.mediavault.mediavault -> app.foundation.url", - "app.modules.mediavault.mediavault -> app.modules", - "app.modules.mediavault.mediavault -> app.modules.mediavault", - "app.modules.mediavault.mediavault -> app.modules.mediavault.api", - "app.modules.mediavault.mediavault -> app.runtime", - "app.modules.mediavault.mediavault -> app.runtime.log", - "app.modules.mediavault.mediavault -> app.schemas", - "app.modules.mediavault.mediavault -> app.schemas.dashboard", - "app.modules.mediavault.mediavault -> app.schemas.mediaserver", - "app.modules.mediavault.mediavault -> app.schemas.types", - "app.modules.mediavault.module -> app.modules", - "app.modules.mediavault.module -> app.modules._base", - "app.modules.mediavault.module -> app.modules._base.mediaserver", - "app.modules.mediavault.module -> app.modules.mediavault", - "app.modules.mediavault.module -> app.modules.mediavault.mediavault", - "app.modules.mediavault.module -> app.runtime", - "app.modules.mediavault.module -> app.runtime.log", - "app.modules.mediavault.module -> app.schemas", - "app.modules.mediavault.module -> app.schemas.dashboard", - "app.modules.mediavault.module -> app.schemas.mediaserver", - "app.modules.mediavault.module -> app.schemas.types", "app.modules.musicbrainz -> app.adapters", "app.modules.musicbrainz -> app.adapters.network", "app.modules.musicbrainz -> app.adapters.network.http", @@ -8282,6 +8250,38 @@ "app.modules.vocechat.vocechat -> app.runtime", "app.modules.vocechat.vocechat -> app.runtime.execution", "app.modules.vocechat.vocechat -> app.runtime.log", + "app.modules.vyo.api -> app.adapters", + "app.modules.vyo.api -> app.adapters.network", + "app.modules.vyo.api -> app.adapters.network.http", + "app.modules.vyo.api -> app.foundation", + "app.modules.vyo.api -> app.foundation.url", + "app.modules.vyo.api -> app.runtime", + "app.modules.vyo.api -> app.runtime.log", + "app.modules.vyo.api -> app.runtime.settings", + "app.modules.vyo.module -> app.modules", + "app.modules.vyo.module -> app.modules._base", + "app.modules.vyo.module -> app.modules._base.mediaserver", + "app.modules.vyo.module -> app.modules.vyo", + "app.modules.vyo.module -> app.modules.vyo.vyo", + "app.modules.vyo.module -> app.runtime", + "app.modules.vyo.module -> app.runtime.log", + "app.modules.vyo.module -> app.schemas", + "app.modules.vyo.module -> app.schemas.dashboard", + "app.modules.vyo.module -> app.schemas.mediaserver", + "app.modules.vyo.module -> app.schemas.types", + "app.modules.vyo.vyo -> app.application", + "app.modules.vyo.vyo -> app.application.mediaserver", + "app.modules.vyo.vyo -> app.foundation", + "app.modules.vyo.vyo -> app.foundation.url", + "app.modules.vyo.vyo -> app.modules", + "app.modules.vyo.vyo -> app.modules.vyo", + "app.modules.vyo.vyo -> app.modules.vyo.api", + "app.modules.vyo.vyo -> app.runtime", + "app.modules.vyo.vyo -> app.runtime.log", + "app.modules.vyo.vyo -> app.schemas", + "app.modules.vyo.vyo -> app.schemas.dashboard", + "app.modules.vyo.vyo -> app.schemas.mediaserver", + "app.modules.vyo.vyo -> app.schemas.types", "app.modules.webpush -> app.adapters", "app.modules.webpush -> app.adapters.network", "app.modules.webpush -> app.adapters.network.webpush", @@ -10916,10 +10916,6 @@ "app.modules.jellyfin.jellyfin", "app.modules.listenbrainz", "app.modules.lrclib", - "app.modules.mediavault", - "app.modules.mediavault.api", - "app.modules.mediavault.mediavault", - "app.modules.mediavault.module", "app.modules.musicbrainz", "app.modules.musicbrainz.cache", "app.modules.navidrome", @@ -10996,6 +10992,10 @@ "app.modules.ugreen.ugreen", "app.modules.vocechat", "app.modules.vocechat.vocechat", + "app.modules.vyo", + "app.modules.vyo.api", + "app.modules.vyo.module", + "app.modules.vyo.vyo", "app.modules.webpush", "app.modules.wechat", "app.modules.wechat.wechat", diff --git a/tests/test_mediavault_module.py b/tests/test_vyo_module.py similarity index 96% rename from tests/test_mediavault_module.py rename to tests/test_vyo_module.py index 232a63c3f..6d0c44db8 100644 --- a/tests/test_mediavault_module.py +++ b/tests/test_vyo_module.py @@ -1,12 +1,12 @@ -"""MediaVault 自建媒体库客户端的行为契约,全部走假 API 不发真实请求。""" +"""Vyo 媒体服务客户端的行为契约,全部走假 API 不发真实请求。""" from pathlib import Path from typing import Optional import pytest -from app.modules.mediavault.api import Api, Result -from app.modules.mediavault.mediavault import MediaVault +from app.modules.vyo.api import Api, Result +from app.modules.vyo.vyo import Vyo from app.schemas.mediaserver import RefreshMediaItem from app.schemas.types import MediaSource, MediaType @@ -40,9 +40,9 @@ class _FakeApi: return handler(params or {}, data) if callable(handler) else handler -def _client(routes: dict, **kwargs) -> MediaVault: +def _client(routes: dict, **kwargs) -> Vyo: """构造一个绕过网络探测的客户端。""" - client = MediaVault.__new__(MediaVault) + client = Vyo.__new__(Vyo) client._host = "http://mv.local" client._playhost = kwargs.get("play_host") client._apikey = "k" @@ -62,7 +62,7 @@ def _item_row(index: int, kind: str = "Movie", **extra) -> dict: def _paged_items(total_rows: list): - """按 page/page_size 切分预置条目,模拟 MediaVault 的分页语义。""" + """按 page/page_size 切分预置条目,模拟 Vyo 的分页语义。""" def handler(params, _data): page = int(params.get("page", 1)) @@ -107,7 +107,7 @@ def test_get_items_always_requests_full_pages(): list(client.get_items("lib-1", limit=130)) sizes = {call["params"]["page_size"] for call in client._api.calls} pages = [call["params"]["page"] for call in client._api.calls] - assert sizes == {MediaVault.PAGE_LIMIT} + assert sizes == {Vyo.PAGE_LIMIT} assert pages == [1, 2] @@ -130,7 +130,7 @@ def test_get_items_stops_when_request_fails(): def test_get_librarys_maps_type_and_builds_image_url(): - """媒体库类型按 MediaVault 的 library_type 映射,封面走带鉴权的图片直链。""" + """媒体库类型按 Vyo 的 library_type 映射,封面走带鉴权的图片直链。""" routes = { "/libraries": Result(True, {"items": [ {"id": "lib-1", "name": "电影库", "library_type": "movies", "root_paths": ["/mnt/movies"]}, @@ -421,7 +421,7 @@ def test_unconfigured_client_never_reports_inactive(): def test_authenticate_posts_to_user_auth_and_returns_token(): - """用户认证走 MediaVault 账号体系,返回访问令牌。""" + """用户认证走 Vyo 账号体系,返回访问令牌。""" routes = {"/login": Result(True, {"access_token": "jwt-token", "token": "legacy"})} client = _client(routes) assert client.authenticate("someone", "secret") == "jwt-token" @@ -449,7 +449,7 @@ def test_disconnect_closes_session(): def test_get_movies_pages_past_the_first_page(): """关键字命中超过单页上限时,仍要能找到排在后面的目标电影。""" - rows = [_item_row(i, title=f"其它{i}") for i in range(MediaVault.PAGE_LIMIT)] + rows = [_item_row(i, title=f"其它{i}") for i in range(Vyo.PAGE_LIMIT)] rows.append(_item_row(999, title="沙丘", year=2021, tmdb_id=438631)) client = _client({"/items": _paged_items(rows)}) @@ -461,7 +461,7 @@ def test_get_movies_pages_past_the_first_page(): def test_find_series_pages_past_the_first_page(): """按标题定位剧集时同样要翻页,否则整部剧会被误判成未入库。""" - rows = [_item_row(i, kind="Series", title=f"其它{i}") for i in range(MediaVault.PAGE_LIMIT)] + rows = [_item_row(i, kind="Series", title=f"其它{i}") for i in range(Vyo.PAGE_LIMIT)] rows.append(_item_row(999, kind="Series", title="剧A", year=2020)) routes = { "/items": _paged_items(rows), @@ -550,7 +550,7 @@ def _failing_second_page(total_rows: list): def test_get_movies_reports_unreachable_when_a_later_page_fails(): """翻页中途断连必须返回 None,不能把残缺结果当成「不在库中」。""" # total 必须超过一页,否则第一页就判定取完,构造不出「中途失败」 - rows = [_item_row(i, title=f"沙丘{i}") for i in range(MediaVault.PAGE_LIMIT + 1)] + rows = [_item_row(i, title=f"沙丘{i}") for i in range(Vyo.PAGE_LIMIT + 1)] client = _client({"/items": _failing_second_page(rows)}) assert client.get_movies(title="沙丘") is None @@ -558,7 +558,7 @@ def test_get_movies_reports_unreachable_when_a_later_page_fails(): def test_find_series_reports_unreachable_when_a_later_page_fails(): """同上:剧集定位不能把断连误判成整部剧未入库。""" - rows = [_item_row(i, kind="Series", title=f"剧{i}") for i in range(MediaVault.PAGE_LIMIT + 1)] + rows = [_item_row(i, kind="Series", title=f"剧{i}") for i in range(Vyo.PAGE_LIMIT + 1)] client = _client({"/items": _failing_second_page(rows)}) assert client.get_tv_episodes(title="剧A") == (None, None) @@ -566,19 +566,19 @@ def test_find_series_reports_unreachable_when_a_later_page_fails(): def test_search_stops_at_reported_total_without_an_extra_request(): """末页恰好满额时用 total 判定取完,不再多发一次可能失败的请求。""" - rows = [_item_row(i, title="沙丘", year=2021) for i in range(MediaVault.PAGE_LIMIT)] + rows = [_item_row(i, title="沙丘", year=2021) for i in range(Vyo.PAGE_LIMIT)] client = _client({"/items": _paged_items(rows)}) matched = client.get_movies(title="沙丘") - assert len(matched) == MediaVault.PAGE_LIMIT + assert len(matched) == Vyo.PAGE_LIMIT assert [call["params"]["page"] for call in client._api.calls] == [1] def test_find_series_returns_first_page_hit_even_if_a_later_page_fails(): """第一页已命中就该直接返回,不因后续页失败被误报成服务不可达。""" rows = [_item_row(0, kind="Series", title="剧A", year=2020)] - rows += [_item_row(i, kind="Series", title=f"其它{i}") for i in range(1, MediaVault.PAGE_LIMIT + 1)] + rows += [_item_row(i, kind="Series", title=f"其它{i}") for i in range(1, Vyo.PAGE_LIMIT + 1)] routes = { "/items": _failing_second_page(rows), "/items/id-0/episodes": Result(True, {"seasons": {"1": [1]}}),