Files
MoviePilot/app/api/endpoints/settings.py

211 lines
7.8 KiB
Python

"""系统设置查询、合同发现和受控更新接口。"""
from typing import Annotated, Any, Optional, Union
from fastapi import Body, Depends, HTTPException, Query
from app.api.context import get_host_runtime
from app.api.dependencies.auth import get_current_active_superuser_async
from app.api.principal import ApiPrincipal
from app.api.response import ResponseAPIRouter
from app.application.configuration import (
get_configured_system_config,
get_runtime_settings,
)
from app.application.settings.service import (
SystemSettingConflictError,
SystemSettingsService,
)
from app.schemas.common import JsonObject as _SchemaJsonObject
from app.schemas.common import ValueData as _SchemaValueData
from app.schemas.response import Response as _SchemaResponse
from app.schemas.system import SystemSettingsUpdateRequest as _SchemaSystemSettingsUpdateRequest
from app.startup.composition.context import HostRuntime
router = ResponseAPIRouter()
def _settings_service(runtime: HostRuntime) -> SystemSettingsService:
"""基于当前宿主运行时构造系统设置应用服务。"""
return SystemSettingsService(
get_runtime_settings(),
get_configured_system_config(),
runtime.system.publish_config_changed,
)
@router.get( # type: ignore[misc]
"/setting/{key}",
summary="查询系统设置",
response_model=_SchemaResponse[_SchemaValueData],
)
async def get_setting(
key: str,
_: ApiPrincipal = Depends(get_current_active_superuser_async),
) -> _SchemaResponse[Any]:
"""查询系统设置(仅管理员)。"""
runtime_settings = get_runtime_settings()
if runtime_settings.contains(key):
value = runtime_settings.get(key)
else:
value = get_configured_system_config().get(key)
return _SchemaResponse(success=True, data={"value": value})
@router.post( # type: ignore[misc]
"/setting/{key}",
summary="更新系统设置",
response_model=_SchemaResponse[None],
)
async def set_setting(
key: str,
value: Annotated[Union[list[Any], dict[str, Any], bool, int, str] | None, Body()] = None,
_: ApiPrincipal = Depends(get_current_active_superuser_async),
runtime: HostRuntime = Depends(get_host_runtime),
) -> _SchemaResponse[Any]:
"""更新系统设置(仅管理员)。"""
result = await runtime.system.update_setting(key, value)
return _SchemaResponse(success=result.success, message=result.message)
@router.get( # type: ignore[misc]
"/settings/catalog",
summary="List system setting contracts",
response_model=_SchemaResponse[_SchemaJsonObject],
)
async def catalog_settings(
group: Annotated[
Optional[str],
Query(description="Setting contract group. Use all to list every registered setting."),
] = "all",
keyword: Annotated[
Optional[str],
Query(description="Case-insensitive substring matched against key, group, label, or description."),
] = None,
source: Annotated[
Optional[str],
Query(description="Optional source filter: settings or systemconfig."),
] = None,
offset: Annotated[int, Query(ge=0, description="Zero-based result offset.")] = 0,
limit: Annotated[int, Query(ge=1, le=500, description="Maximum contracts to return.")] = 100,
_: ApiPrincipal = Depends(get_current_active_superuser_async),
runtime: HostRuntime = Depends(get_host_runtime),
) -> _SchemaResponse[Any]:
"""分页返回轻量设置合同,供 Agent 建立可用设置索引。"""
try:
data = _settings_service(runtime).catalog(
group=group,
keyword=keyword,
source=source,
offset=offset,
limit=limit,
)
except ValueError as error:
return _SchemaResponse(success=False, message=str(error))
return _SchemaResponse(success=True, data=data)
@router.get( # type: ignore[misc]
"/settings/describe/{setting_key}",
summary="Describe one system setting contract",
response_model=_SchemaResponse[_SchemaJsonObject],
)
async def describe_setting(
setting_key: str,
include_value: Annotated[
bool,
Query(description="Return the current value in addition to the complete contract."),
] = True,
show_secrets: Annotated[
bool,
Query(description="Return unredacted secret values only when explicitly authorized."),
] = False,
_: ApiPrincipal = Depends(get_current_active_superuser_async),
runtime: HostRuntime = Depends(get_host_runtime),
) -> _SchemaResponse[Any]:
"""返回单项完整设置合同、当前值和 revision。"""
try:
data = _settings_service(runtime).describe(
setting_key=setting_key,
include_value=include_value,
show_secrets=show_secrets,
)
except ValueError as error:
return _SchemaResponse(success=False, message=str(error))
return _SchemaResponse(success=True, data=data)
@router.get( # type: ignore[misc]
"/settings",
summary="Discover or read registered system settings",
response_model=_SchemaResponse[_SchemaJsonObject],
)
async def query_settings(
setting_key: Annotated[
Optional[str],
Query(
description=(
"Exact setting key. Accepts Settings field names such as APP_DOMAIN or LLM_MODEL, "
"SystemConfigKey values or enum names such as Downloaders or MediaServers, and "
"aliases that resolve to one unique setting. Omit it to use list-style discovery."
)
),
] = None,
group: Annotated[
Optional[str],
Query(
description=(
"Discovery group used when setting_key is omitted. Use config.system.list for a complete, "
"machine-readable group index; this compatibility endpoint accepts all registered catalog groups, "
"including settings and systemconfig."
)
),
] = "all",
keyword: Annotated[
Optional[str],
Query(description="Case-insensitive substring used to discover matching keys, groups, or labels."),
] = None,
include_values: Annotated[
Optional[bool],
Query(description="Return full values. Defaults to true for one exact key and false for discovery results."),
] = None,
show_secrets: Annotated[
bool,
Query(description="Return unredacted secret values. Defaults to false and remains confirmation-protected."),
] = False,
_: ApiPrincipal = Depends(get_current_active_superuser_async),
runtime: HostRuntime = Depends(get_host_runtime),
) -> _SchemaResponse[Any]:
"""按登记元数据查询设置,并默认对敏感值递归脱敏。"""
try:
data = _settings_service(runtime).query(
setting_key=setting_key,
group=group,
keyword=keyword,
include_values=include_values,
show_secrets=show_secrets,
)
except ValueError as error:
return _SchemaResponse(success=False, message=str(error))
return _SchemaResponse(success=True, data=data)
@router.post( # type: ignore[misc]
"/settings",
summary="Update one registered system setting",
response_model=_SchemaResponse[_SchemaJsonObject],
)
async def update_settings(
payload: _SchemaSystemSettingsUpdateRequest,
_: ApiPrincipal = Depends(get_current_active_superuser_async),
runtime: HostRuntime = Depends(get_host_runtime),
) -> _SchemaResponse[Any]:
"""按替换、字典合并或列表项操作更新一个登记设置。"""
try:
data = await _settings_service(runtime).update(**payload.model_dump())
except SystemSettingConflictError as error:
raise HTTPException(status_code=409, detail=str(error)) from error
except ValueError as error:
return _SchemaResponse(success=False, message=str(error))
return _SchemaResponse(success=True, message=data.get("message"), data=data)