mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
refactor: complete runtime configuration migration
This commit is contained in:
@@ -7,6 +7,8 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
class SystemConfigReader(Protocol):
|
||||
"""持久化用户配置的最小只读端口。"""
|
||||
@@ -35,6 +37,34 @@ class ConfigurationRepository(SystemConfigReader, SystemConfigWriter, Protocol):
|
||||
"""兼容同时提供读写能力的旧配置仓储。"""
|
||||
|
||||
|
||||
class MutableRuntimeSettings(Protocol):
|
||||
"""部署设置对象对管理 API 暴露的最小可变合同。"""
|
||||
|
||||
def model_dump(
|
||||
self,
|
||||
*,
|
||||
include: Optional[set[str]] = None,
|
||||
exclude: Optional[set[str]] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""按白名单或排除列表导出当前设置。"""
|
||||
...
|
||||
|
||||
def update_settings(
|
||||
self,
|
||||
env: dict[str, Any],
|
||||
) -> dict[str, tuple[Optional[bool], str]]:
|
||||
"""批量更新部署设置并返回逐项结果。"""
|
||||
...
|
||||
|
||||
def update_setting(
|
||||
self,
|
||||
key: str,
|
||||
value: Any,
|
||||
) -> tuple[Optional[bool], str]:
|
||||
"""更新单个部署设置。"""
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TransferRetryConfig:
|
||||
"""整理失败重试用例在一次调用中使用的稳定配置快照。"""
|
||||
@@ -54,6 +84,28 @@ class ApiRuntimeConfig:
|
||||
temp_path: Path = Path(".")
|
||||
media_recognize_share: bool = False
|
||||
subscribe_mode: str = "spider"
|
||||
search_source: str = ""
|
||||
media_extensions: tuple[str, ...] = ()
|
||||
subtitle_extensions: tuple[str, ...] = ()
|
||||
audio_extensions: tuple[str, ...] = ()
|
||||
movie_rename_format: str = ""
|
||||
television_rename_format: str = ""
|
||||
music_rename_format: str = ""
|
||||
vapid_private_key: str = ""
|
||||
vapid_subject: str = ""
|
||||
cookiecloud_enable_local: bool = False
|
||||
cookiecloud_auth_header: Optional[str] = None
|
||||
cookie_path: Path = Path(".")
|
||||
root_path: Path = Path(".")
|
||||
version_flag: str = "v3"
|
||||
|
||||
def rename_format(self, media_type: MediaType) -> str:
|
||||
"""从请求快照返回指定媒体类型的稳定重命名格式。"""
|
||||
if media_type == MediaType.TV:
|
||||
return self.television_rename_format
|
||||
if media_type == MediaType.MUSIC:
|
||||
return self.music_rename_format
|
||||
return self.movie_rename_format
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -85,22 +137,80 @@ class ChainRuntimeConfig:
|
||||
"""Chain 在一次宿主生命周期内使用的基础配置快照。"""
|
||||
|
||||
media_extensions: tuple[str, ...]
|
||||
video_extensions: tuple[str, ...] = ()
|
||||
subtitle_extensions: tuple[str, ...] = ()
|
||||
audio_extensions: tuple[str, ...] = ()
|
||||
temporary_path: Path = Path(".")
|
||||
root_path: Path = Path(".")
|
||||
config_path: Path = Path(".")
|
||||
frontend_path: Path = Path(".")
|
||||
superuser: str = "admin"
|
||||
media_recognize_share: bool = False
|
||||
auxiliary_auth_enable: bool = False
|
||||
global_image_cache: bool = False
|
||||
download_subtitle: bool = True
|
||||
music_metadata_to_simplified: bool = True
|
||||
recognize_plugin_first: bool = False
|
||||
ai_agent_enable: bool = False
|
||||
ai_agent_global: bool = False
|
||||
ai_agent_retry_transfer: bool = False
|
||||
llm_provider: str = ""
|
||||
llm_model: str = ""
|
||||
search_resource_pages: int = 1
|
||||
ai_recommend_enabled: bool = False
|
||||
ai_recommend_max_items: int = 50
|
||||
ai_recommend_user_preference: str = ""
|
||||
max_search_name_limit: int = 3
|
||||
search_multiple_name: bool = False
|
||||
search_threadpool_size: int = 1
|
||||
transfer_threads: int = 1
|
||||
transfer_failure_notification_aggregation: bool = True
|
||||
transfer_task_timeout: int = 120
|
||||
scrape_follow_tmdb: bool = True
|
||||
metadata_cache_ttl: int = 3600
|
||||
auto_download_user: Optional[str] = None
|
||||
resource_url: Optional[str] = None
|
||||
history_url: Optional[str] = None
|
||||
downloading_url: Optional[str] = None
|
||||
movie_subscribe_url: Optional[str] = None
|
||||
television_subscribe_url: Optional[str] = None
|
||||
music_subscribe_url: Optional[str] = None
|
||||
user_agent: str = ""
|
||||
normal_user_agent: str = ""
|
||||
proxy: Any = None
|
||||
proxy_server: Any = None
|
||||
proxy_host: Optional[str] = None
|
||||
github_headers: Any = None
|
||||
cookiecloud_blacklist: Any = None
|
||||
subscribe_mode: str = "spider"
|
||||
no_cache_site_key: str = ""
|
||||
refresh_batch_size: int = 50
|
||||
torrent_cache_size: int = 1000
|
||||
site_url: Optional[str] = None
|
||||
season_zero_names: tuple[str, ...] = ()
|
||||
movie_rename_format: str = ""
|
||||
television_rename_format: str = ""
|
||||
music_rename_format: str = ""
|
||||
tmdb_image_domain: str = "image.tmdb.org"
|
||||
|
||||
def rename_format(self, media_type: MediaType) -> str:
|
||||
"""从快照返回指定媒体类型的稳定重命名格式。"""
|
||||
if media_type == MediaType.TV:
|
||||
return self.television_rename_format
|
||||
if media_type == MediaType.MUSIC:
|
||||
return self.music_rename_format
|
||||
return self.movie_rename_format
|
||||
|
||||
def tmdb_image_url(
|
||||
self,
|
||||
file_path: Optional[str],
|
||||
file_size: str = "original",
|
||||
) -> Optional[str]:
|
||||
"""使用快照中的 TMDB 图片域名构造完整图片地址。"""
|
||||
if not file_path:
|
||||
return None
|
||||
normalized_path = file_path.removeprefix("/")
|
||||
return f"https://{self.tmdb_image_domain}/t/p/{file_size}/{normalized_path}"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -112,6 +222,42 @@ class RuntimeConfiguration:
|
||||
chain: Callable[[], ChainRuntimeConfig]
|
||||
|
||||
|
||||
class RuntimeSettingsService:
|
||||
"""隔离管理 API 与全局 Settings 实例的读写适配服务。"""
|
||||
|
||||
def __init__(self, settings: MutableRuntimeSettings) -> None:
|
||||
"""保存由 Startup 注入的唯一部署设置对象。"""
|
||||
self._settings = settings
|
||||
|
||||
def contains(self, key: str) -> bool:
|
||||
"""判断部署设置是否声明指定字段或属性。"""
|
||||
return hasattr(self._settings, key)
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""读取一个部署设置,缺失时返回调用方默认值。"""
|
||||
return getattr(self._settings, key, default)
|
||||
|
||||
def snapshot(
|
||||
self,
|
||||
*,
|
||||
include: Optional[set[str]] = None,
|
||||
exclude: Optional[set[str]] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""导出脱离可变 Settings 对象的请求级字典快照。"""
|
||||
return self._settings.model_dump(include=include, exclude=exclude)
|
||||
|
||||
def update_many(
|
||||
self,
|
||||
env: dict[str, Any],
|
||||
) -> dict[str, tuple[Optional[bool], str]]:
|
||||
"""批量更新部署设置。"""
|
||||
return self._settings.update_settings(env=env)
|
||||
|
||||
def update(self, key: str, value: Any) -> tuple[Optional[bool], str]:
|
||||
"""更新单个部署设置。"""
|
||||
return self._settings.update_setting(key=key, value=value)
|
||||
|
||||
|
||||
class SystemConfigService:
|
||||
"""系统配置读写应用服务。"""
|
||||
|
||||
@@ -154,6 +300,7 @@ class SystemConfigService:
|
||||
_configured_system_config: SystemConfigService | None = None
|
||||
_transfer_retry_config_provider: Callable[[], TransferRetryConfig] | None = None
|
||||
_runtime_configuration: RuntimeConfiguration | None = None
|
||||
_runtime_settings_service: RuntimeSettingsService | None = None
|
||||
|
||||
|
||||
def configure_system_config(service: SystemConfigService) -> None:
|
||||
@@ -190,6 +337,19 @@ def configure_runtime_configuration(configuration: RuntimeConfiguration) -> None
|
||||
_runtime_configuration = configuration
|
||||
|
||||
|
||||
def configure_runtime_settings(service: RuntimeSettingsService) -> None:
|
||||
"""由组合根登记管理 API 使用的部署设置服务。"""
|
||||
global _runtime_settings_service
|
||||
_runtime_settings_service = service
|
||||
|
||||
|
||||
def get_runtime_settings() -> RuntimeSettingsService:
|
||||
"""返回组合根登记的部署设置服务。"""
|
||||
if _runtime_settings_service is None:
|
||||
raise RuntimeError("部署设置服务尚未装配")
|
||||
return _runtime_settings_service
|
||||
|
||||
|
||||
def get_scheduler_runtime_config() -> SchedulerRuntimeConfig:
|
||||
"""为一次调度操作创建不可变配置快照。"""
|
||||
if _runtime_configuration is None:
|
||||
@@ -202,3 +362,10 @@ def get_api_runtime_config_snapshot() -> ApiRuntimeConfig:
|
||||
if _runtime_configuration is None:
|
||||
raise RuntimeError("运行时配置尚未装配")
|
||||
return _runtime_configuration.api()
|
||||
|
||||
|
||||
def get_chain_runtime_config_snapshot() -> ChainRuntimeConfig:
|
||||
"""为无实例兼容入口创建一次稳定的 Chain 配置快照。"""
|
||||
if _runtime_configuration is None:
|
||||
raise RuntimeError("运行时配置尚未装配")
|
||||
return _runtime_configuration.chain()
|
||||
|
||||
Reference in New Issue
Block a user