mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-30 03:48:03 +08:00
114 lines
4.3 KiB
Python
114 lines
4.3 KiB
Python
"""分类策略运行时门面及 legacy 只读投影。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
from typing import Optional, cast
|
|
|
|
from app.application.classification.configuration import (
|
|
ClassificationPolicyConfigurationService,
|
|
ClassificationPolicyNotInitializedError,
|
|
)
|
|
from app.application.classification.contract import ClassificationPolicyConflictError
|
|
from app.application.classification.legacy import (
|
|
project_policy_to_legacy_category_config,
|
|
)
|
|
from app.schemas.category import (
|
|
CategoryConfig,
|
|
ClassificationPolicy,
|
|
ClassificationValidationIssue,
|
|
MediaCategoryMap,
|
|
)
|
|
|
|
|
|
class ClassificationRuntime:
|
|
"""
|
|
向 API 和业务链暴露当前分类策略,并封装 legacy 迁移失败回退。
|
|
|
|
持久化和 revision 并发控制由配置服务负责;本门面只协调动态字段登记和兼容投影。
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
service: ClassificationPolicyConfigurationService,
|
|
*,
|
|
legacy_config: Optional[CategoryConfig] = None,
|
|
diagnostics: tuple[ClassificationValidationIssue, ...] = (),
|
|
) -> None:
|
|
"""绑定策略服务,并保存未成功迁移时的只读 legacy 快照。"""
|
|
self._service = service
|
|
self._legacy_config = (legacy_config or CategoryConfig()).model_copy(deep=True)
|
|
self._diagnostics = tuple(issue.model_copy(deep=True) for issue in diagnostics)
|
|
self._lock = threading.RLock()
|
|
|
|
@property
|
|
def service(self) -> ClassificationPolicyConfigurationService:
|
|
"""返回当前 lifespan 唯一的策略配置服务。"""
|
|
return self._service
|
|
|
|
def active_policy(self) -> Optional[ClassificationPolicy]:
|
|
"""返回活动策略;尚未成功迁移或初始化时返回 None。"""
|
|
try:
|
|
return self._service.active()
|
|
except ClassificationPolicyNotInitializedError:
|
|
return None
|
|
|
|
def require_policy(self) -> ClassificationPolicy:
|
|
"""返回活动策略;不可用时保留配置服务的明确未初始化异常。"""
|
|
return self._service.active()
|
|
|
|
def diagnostics(self) -> tuple[ClassificationValidationIssue, ...]:
|
|
"""返回与内部引用隔离的当前迁移或发布诊断。"""
|
|
with self._lock:
|
|
return tuple(issue.model_copy(deep=True) for issue in self._diagnostics)
|
|
|
|
def legacy_config(self) -> CategoryConfig:
|
|
"""把活动策略投影为旧配置;策略不可用时返回启动时 legacy 快照。"""
|
|
policy = self.active_policy()
|
|
if policy is not None:
|
|
return project_policy_to_legacy_category_config(policy)
|
|
with self._lock:
|
|
return cast(CategoryConfig, self._legacy_config.model_copy(deep=True))
|
|
|
|
def media_categories(self) -> MediaCategoryMap:
|
|
"""按策略顺序返回电影、电视剧和音乐的启用分类名称。"""
|
|
categories: dict[str, list[str]] = {
|
|
"电影": [],
|
|
"电视剧": [],
|
|
"音乐": [],
|
|
}
|
|
policy = self.active_policy()
|
|
if policy is None:
|
|
legacy = self.legacy_config()
|
|
categories["电影"] = list((legacy.movie or {}).keys())
|
|
categories["电视剧"] = list((legacy.tv or {}).keys())
|
|
return MediaCategoryMap(categories)
|
|
for category in policy.categories:
|
|
if category.enabled and category.name not in categories[category.media_type]:
|
|
categories[category.media_type].append(category.name)
|
|
return MediaCategoryMap(categories)
|
|
|
|
async def publish_policy(
|
|
self,
|
|
policy: ClassificationPolicy,
|
|
*,
|
|
expected_revision: int,
|
|
) -> ClassificationPolicy:
|
|
"""发布新版策略;未迁移的空存储允许以 revision 0 初始化恢复。"""
|
|
current = self.active_policy()
|
|
if current is None:
|
|
if expected_revision != 0:
|
|
raise ClassificationPolicyConflictError(
|
|
expected_revision=expected_revision,
|
|
current_revision=0,
|
|
)
|
|
published = await self._service.async_initialize(policy)
|
|
else:
|
|
published = await self._service.async_publish(
|
|
policy,
|
|
expected_revision=expected_revision,
|
|
)
|
|
with self._lock:
|
|
self._diagnostics = ()
|
|
return published
|