mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-02 21:34:32 +08:00
fix(classification): initialize defaults in fresh v3 environments
This commit is contained in:
@@ -14,6 +14,7 @@ from pydantic import ValidationError
|
||||
from app.application.classification.configuration import (
|
||||
ClassificationPolicyConfigurationService,
|
||||
ClassificationPolicyValidationError,
|
||||
build_default_classification_policy,
|
||||
needs_default_music_classification,
|
||||
with_default_music_classification,
|
||||
)
|
||||
@@ -95,7 +96,10 @@ async def compose_classification(
|
||||
enrichment: ClassificationEnrichmentPort | None = None,
|
||||
) -> ClassificationComposition:
|
||||
"""
|
||||
构造分类策略服务,并仅在新配置键不存在时读取一次 legacy YAML
|
||||
构造分类策略服务,并仅在新配置键不存在时读取一次 legacy YAML。
|
||||
|
||||
新环境没有旧配置文件时,启动层会显式发布内置默认策略;已有策略或合法
|
||||
legacy 配置始终优先,避免用默认值覆盖用户事实。
|
||||
|
||||
已存在但损坏的新策略不会重新读取 YAML 或覆盖数据库事实,运行时保持不可用并暴露诊断。
|
||||
"""
|
||||
@@ -189,7 +193,8 @@ async def compose_classification(
|
||||
|
||||
legacy_path = Path(settings.CONFIG_PATH) / "category.yaml"
|
||||
if not await executor.run(legacy_path.exists):
|
||||
await service.async_initialize()
|
||||
await service.async_initialize(build_default_classification_policy())
|
||||
logger.info("未发现 category.yaml,已初始化内置默认分类策略 revision 1")
|
||||
return finish(
|
||||
ClassificationRuntime(service),
|
||||
migrated=False,
|
||||
|
||||
@@ -118,7 +118,7 @@ V2 自动分类主要由 TMDB 详情和 `category.yaml` 驱动,只覆盖电影
|
||||
|
||||
目录、订阅、下载和整理历史不再只保存易变的分类名称,而是同时记录内部分类编号 `category_id` 和当时的路径快照。分类改名或调整路径后,既有目录和订阅仍能找到同一个分类;已经建立的整理计划会继续使用计划创建时保存的分类目标,不会被后续策略修改。
|
||||
|
||||
升级时,如果尚未存在 V3 分类策略,系统会读取现有 `category.yaml` 并自动迁移;旧 TMDB 规则的顺序、排除条件、年份范围和“没有规则命中时使用的默认分类”会保留。迁移完成后不再继续写入 YAML。旧 `GET /api/v1/media/category` 和 `GET /api/v1/media/category/config` 暂时保留为只读投影,旧 `POST /api/v1/media/category/config` 已移除,所有新写入都通过带版本号校验的策略接口完成。
|
||||
升级时,如果尚未存在 V3 分类策略,系统会优先读取现有 `category.yaml` 并自动迁移;没有旧配置文件的新环境会直接初始化内置的电影、电视剧和音乐默认分类。旧 TMDB 规则的顺序、排除条件、年份范围和“没有规则命中时使用的默认分类”会保留。迁移完成后不再继续写入 YAML。旧 `GET /api/v1/media/category` 和 `GET /api/v1/media/category/config` 暂时保留为只读投影,旧 `POST /api/v1/media/category/config` 已移除,所有新写入都通过带版本号校验的策略接口完成。
|
||||
|
||||
旧分类名称中用于表示目录层级的 `/` 会在迁移时转换为多个安全路径段,分类名称和目录显示结果保持不变;已经完成迁移的策略会在数据库升级时自动修复。
|
||||
|
||||
|
||||
@@ -386,6 +386,73 @@ async def test_edited_legacy_default_policy_is_not_automatically_changed(
|
||||
assert store.write_count == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio # type: ignore[misc]
|
||||
async def test_fresh_environment_initializes_builtin_default_policy(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""没有旧配置文件的新环境应持久化完整内置分类,而不是只有未分类兜底。"""
|
||||
store = _MemoryPolicyStore()
|
||||
monkeypatch.setattr(
|
||||
classification_composition,
|
||||
"SystemConfigClassificationPolicyStore",
|
||||
lambda *_args: store,
|
||||
)
|
||||
|
||||
composition = await classification_composition.compose_classification(
|
||||
executor=cast(Any, _InlineExecutor()),
|
||||
settings=cast(Any, SimpleNamespace(CONFIG_PATH=tmp_path)),
|
||||
system_config=cast(Any, _SystemConfig({})),
|
||||
)
|
||||
|
||||
policy = composition.runtime.require_policy()
|
||||
assert composition.migrated is False
|
||||
assert policy.revision == 1
|
||||
assert [category.id for category in policy.categories] == [
|
||||
"movie.animation",
|
||||
"movie.chinese",
|
||||
"movie.foreign",
|
||||
"tv.dongman.cn",
|
||||
"tv.dongman.jp",
|
||||
"tv.documentary",
|
||||
"tv.kids",
|
||||
"tv.variety",
|
||||
"tv.chinese",
|
||||
"tv.western",
|
||||
"tv.asian",
|
||||
"tv.uncategorized",
|
||||
"music.uncategorized",
|
||||
"music.album",
|
||||
"music.compilation",
|
||||
"music.ep",
|
||||
"music.single",
|
||||
]
|
||||
assert [rule.id for rule in policy.rules] == [
|
||||
"movie.animation.default",
|
||||
"movie.chinese.default",
|
||||
"tv.dongman.cn.default",
|
||||
"tv.dongman.jp.default",
|
||||
"tv.documentary.default",
|
||||
"tv.kids.default",
|
||||
"tv.variety.default",
|
||||
"tv.chinese.default",
|
||||
"tv.western.default",
|
||||
"tv.asian.default",
|
||||
"music.compilation.default",
|
||||
"music.ep.default",
|
||||
"music.single.default",
|
||||
"music.album.default",
|
||||
]
|
||||
assert policy.fallbacks == {
|
||||
"电影": "movie.foreign",
|
||||
"电视剧": "tv.uncategorized",
|
||||
"音乐": "music.uncategorized",
|
||||
}
|
||||
assert store.state is not None
|
||||
assert store.state.active == policy
|
||||
assert store.write_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio # type: ignore[misc]
|
||||
async def test_absent_policy_migrates_yaml_once_without_rewriting_file(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
Reference in New Issue
Block a user