From e359bbace9edc462b8f357e977ff44107f39228f Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 24 Aug 2026 01:33:04 +0800 Subject: [PATCH] refactor: type media server contracts --- app/runtime/extensions/module/contracts.py | 11 +- .../runtime-contract-baseline.json | 121 +++++++++++------- tests/test_module_method_contracts.py | 30 +++++ 3 files changed, 117 insertions(+), 45 deletions(-) diff --git a/app/runtime/extensions/module/contracts.py b/app/runtime/extensions/module/contracts.py index f655d2904..4f434d2da 100644 --- a/app/runtime/extensions/module/contracts.py +++ b/app/runtime/extensions/module/contracts.py @@ -87,10 +87,19 @@ _METHOD_CONTRACTS = { ), "obtain_images": ModuleMethodContract(family="media-recognition", input_contract="MediaInfo", result_contract="MediaInfo | None", aggregation=ModuleResultAggregation.PIPELINE_RELAY, required_parameters=("mediainfo",)), "media_category": ModuleMethodContract(family="media-recognition", input_contract="MediaCategoryRequest", result_contract="CategoryConfig | None"), - "mediaserver_items": ModuleMethodContract(family="media-server", input_contract="MediaServerItemsRequest", result_contract="list[MediaServerItem]", aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server", "library_id", "start_index", "limit")), + "mediaserver_items": ModuleMethodContract(family="media-server", input_contract="MediaServerItemsRequest", result_contract="Iterable[MediaServerItem] | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("server", "library_id", "start_index", "limit")), "mediaserver_iteminfo": ModuleMethodContract(family="media-server", input_contract="MediaServerItemRequest", result_contract="MediaServerItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("server", "item_id")), "mediaserver_play_url": ModuleMethodContract(family="media-server", input_contract="MediaServerPlayRequest", result_contract="str | None", result_shape=ModuleResultShape.STRING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("server", "item_id")), "mediaserver_tv_episodes": ModuleMethodContract(family="media-server", input_contract="MediaServerEpisodesRequest", result_contract="list[MediaServerPlayItem]", aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server", "item_id")), + "media_exists": ModuleMethodContract(family="media-server", input_contract="MediaExistsRequest", result_contract="ExistMediaInfo | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("mediainfo", "itemid", "server")), + "media_statistic": ModuleMethodContract(family="media-server", input_contract="MediaStatisticRequest", result_contract="list[Statistic]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server",)), + "mediaserver_image_cookies": ModuleMethodContract(family="media-server", input_contract="MediaServerImageCookiesRequest", result_contract="str | dict | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("server", "image_url")), + "mediaserver_items_count": ModuleMethodContract(family="media-server", input_contract="MediaServerItemsCountRequest", result_contract="int | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("server", "library_id")), + "mediaserver_latest": ModuleMethodContract(family="media-server", input_contract="MediaServerRecentRequest", result_contract="list[MediaServerPlayItem]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server", "count", "username")), + "mediaserver_latest_images": ModuleMethodContract(family="media-server", input_contract="MediaServerRecentImagesRequest", result_contract="list[str]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server", "count", "remote", "username")), + "mediaserver_librarys": ModuleMethodContract(family="media-server", input_contract="MediaServerLibrariesRequest", result_contract="list[MediaServerLibrary]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server", "username", "hidden")), + "mediaserver_playing": ModuleMethodContract(family="media-server", input_contract="MediaServerRecentRequest", result_contract="list[MediaServerPlayItem]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server", "count", "username")), + "mediaserver_season_episode_ids": ModuleMethodContract(family="media-server", input_contract="MediaServerSeasonEpisodesRequest", result_contract="dict[int, str] | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("server", "item_id", "season")), "download_file": ModuleMethodContract(family="storage", input_contract="StorageDownloadRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "path")), "upload_file": ModuleMethodContract(family="storage", input_contract="StorageUploadRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "path", "new_name")), "list_files": ModuleMethodContract(family="storage", input_contract="StorageListRequest", result_contract="list[FileItem]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("fileitem", "recursion")), diff --git a/tests/fixtures/architecture/runtime-contract-baseline.json b/tests/fixtures/architecture/runtime-contract-baseline.json index 7de4dc104..2bb4183bc 100644 --- a/tests/fixtures/architecture/runtime-contract-baseline.json +++ b/tests/fixtures/architecture/runtime-contract-baseline.json @@ -4385,15 +4385,19 @@ "version": 1 }, "media_exists": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-server", - "input_contract": "MediaServerKeywordArguments", + "input_contract": "MediaExistsRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaServerProviderResult", + "required_parameters": [ + "itemid", + "mediainfo", + "server" + ], + "result_contract": "ExistMediaInfo | None", "result_shape": "any", "supports_async": true, "supports_sync": true, @@ -4417,31 +4421,36 @@ "version": 1 }, "media_statistic": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-server", - "input_contract": "MediaServerKeywordArguments", + "input_contract": "MediaStatisticRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaServerProviderResult", - "result_shape": "any", + "required_parameters": [ + "server" + ], + "result_contract": "list[Statistic]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "mediaserver_image_cookies": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-server", - "input_contract": "MediaServerKeywordArguments", + "input_contract": "MediaServerImageCookiesRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaServerProviderResult", + "required_parameters": [ + "image_url", + "server" + ], + "result_contract": "str | dict | None", "result_shape": "any", "supports_async": true, "supports_sync": true, @@ -4468,7 +4477,7 @@ "version": 1 }, "mediaserver_items": { - "aggregation": "ordered_list_merge", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-server", @@ -4481,7 +4490,7 @@ "server", "start_index" ], - "result_contract": "list[MediaServerItem]", + "result_contract": "Iterable[MediaServerItem] | None", "result_shape": "any", "supports_async": true, "supports_sync": true, @@ -4489,15 +4498,18 @@ "version": 1 }, "mediaserver_items_count": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-server", - "input_contract": "MediaServerKeywordArguments", + "input_contract": "MediaServerItemsCountRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaServerProviderResult", + "required_parameters": [ + "library_id", + "server" + ], + "result_contract": "int | None", "result_shape": "any", "supports_async": true, "supports_sync": true, @@ -4505,48 +4517,61 @@ "version": 1 }, "mediaserver_latest": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-server", - "input_contract": "MediaServerKeywordArguments", + "input_contract": "MediaServerRecentRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaServerProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "server", + "username" + ], + "result_contract": "list[MediaServerPlayItem]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "mediaserver_latest_images": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-server", - "input_contract": "MediaServerKeywordArguments", + "input_contract": "MediaServerRecentImagesRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaServerProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "remote", + "server", + "username" + ], + "result_contract": "list[str]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "mediaserver_librarys": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-server", - "input_contract": "MediaServerKeywordArguments", + "input_contract": "MediaServerLibrariesRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaServerProviderResult", - "result_shape": "any", + "required_parameters": [ + "hidden", + "server", + "username" + ], + "result_contract": "list[MediaServerLibrary]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -4572,32 +4597,40 @@ "version": 1 }, "mediaserver_playing": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-server", - "input_contract": "MediaServerKeywordArguments", + "input_contract": "MediaServerRecentRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaServerProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "server", + "username" + ], + "result_contract": "list[MediaServerPlayItem]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "mediaserver_season_episode_ids": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-server", - "input_contract": "MediaServerKeywordArguments", + "input_contract": "MediaServerSeasonEpisodesRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaServerProviderResult", - "result_shape": "any", + "required_parameters": [ + "item_id", + "season", + "server" + ], + "result_contract": "dict[int, str] | None", + "result_shape": "mapping", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", diff --git a/tests/test_module_method_contracts.py b/tests/test_module_method_contracts.py index 267b5a57c..408d768ea 100644 --- a/tests/test_module_method_contracts.py +++ b/tests/test_module_method_contracts.py @@ -159,6 +159,36 @@ def test_downloader_query_contracts_merge_provider_lists() -> None: assert contract.result_shape is ModuleResultShape.LIST +def test_media_server_contracts_distinguish_streams_lists_and_scalar_routes() -> None: + """媒体服务器能力应按真实返回形状选择合并或目标路由语义。""" + list_methods = { + "media_statistic", + "mediaserver_latest", + "mediaserver_latest_images", + "mediaserver_librarys", + "mediaserver_playing", + } + scalar_methods = { + "media_exists", + "mediaserver_image_cookies", + "mediaserver_items_count", + "mediaserver_season_episode_ids", + } + + for method in list_methods: + contract = get_module_method_contract(method) + assert contract.aggregation is ModuleResultAggregation.ORDERED_LIST_MERGE + assert contract.result_shape is ModuleResultShape.LIST + for method in scalar_methods: + contract = get_module_method_contract(method) + assert contract.aggregation is ModuleResultAggregation.FIRST_NON_EMPTY + + items = get_module_method_contract("mediaserver_items") + assert items.result_contract == "Iterable[MediaServerItem] | None" + assert items.aggregation is ModuleResultAggregation.FIRST_NON_EMPTY + assert items.result_shape is ModuleResultShape.ANY + + def test_heterogeneous_torrent_files_result_remains_legacy_compatible() -> None: """下载器文件集合尚未归一前不得声明虚假的列表聚合语义。""" contract = get_module_method_contract("torrent_files")