mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-04 14:24:58 +08:00
fix(classification): 补充元数据不再把已命中的自动分类打成兜底 (#6717)
This commit is contained in:
@@ -314,9 +314,12 @@ class ClassificationExecutionService:
|
||||
*,
|
||||
effective_override: ClassificationSelection | None,
|
||||
) -> ClassificationSubject:
|
||||
"""应用纯求值结果和人工覆盖,并更新兼容目录分类。"""
|
||||
"""应用纯求值结果、降级保护和人工覆盖,并更新兼容目录分类。"""
|
||||
evaluation = evaluate_classification_facts(policy, facts)
|
||||
result = evaluation.result.model_copy(deep=True)
|
||||
carried = _degraded_automatic_selection(finalized, policy, result)
|
||||
if carried is not None:
|
||||
result.effective = carried
|
||||
if effective_override:
|
||||
result.effective = effective_override.model_copy(deep=True)
|
||||
finalized.classification = result
|
||||
@@ -537,6 +540,38 @@ def _explicit_effective_override(
|
||||
return deepcopy(effective)
|
||||
|
||||
|
||||
def _degraded_automatic_selection(
|
||||
media: ClassificationSubject,
|
||||
policy: ClassificationPolicy,
|
||||
result: ClassificationResult,
|
||||
) -> ClassificationSelection | None:
|
||||
"""事实缺失把同策略结果打成兜底时,返回可复用的既有自动分类选择。
|
||||
|
||||
补充元数据的入口会先裁剪媒体字段再重新分类,此时缺失事实只会让结果更差,
|
||||
不会带来新的判断依据。因此仅在既有快照来自同一策略修订且命中过规则,而本次
|
||||
求值同时出现事实缺失和兜底化时才复用;事实读齐后给出的兜底结果仍然生效,
|
||||
策略调整和真正的重新分类不会被掩盖。
|
||||
"""
|
||||
if result.state != "partial":
|
||||
return None
|
||||
proposed = result.effective or result.recommended
|
||||
if proposed is not None and proposed.source != "fallback":
|
||||
return None
|
||||
carried = getattr(media, "classification", None)
|
||||
if not isinstance(carried, ClassificationResult):
|
||||
return None
|
||||
if carried.policy_revision != policy.revision:
|
||||
return None
|
||||
if carried.state not in {"complete", "partial"}:
|
||||
return None
|
||||
selection = carried.effective or carried.recommended
|
||||
if selection is None or selection.source != "automatic":
|
||||
return None
|
||||
if not str(selection.category_id or "").strip():
|
||||
return None
|
||||
return selection.model_copy(deep=True)
|
||||
|
||||
|
||||
def _enum_text(value: object) -> str:
|
||||
"""把字符串枚举或普通值转换为去空白文本。"""
|
||||
if isinstance(value, Enum):
|
||||
|
||||
@@ -334,6 +334,69 @@ def test_execution_refreshes_same_revision_after_auxiliary_facts_change() -> Non
|
||||
assert refreshed.classification.effective.rule_id == "rule.movie.jp"
|
||||
|
||||
|
||||
def test_execution_preserves_automatic_category_when_supplement_drops_facts() -> None:
|
||||
"""裁剪过字段的媒体再次分类时,同策略已命中的自动分类不得被兜底覆盖。"""
|
||||
migration = migrate_legacy_category_config(
|
||||
{
|
||||
"movie": {},
|
||||
"tv": {
|
||||
"国产剧": {"origin_country": "CN,TW,HK"},
|
||||
"未分类": None,
|
||||
},
|
||||
}
|
||||
)
|
||||
domestic = next(
|
||||
category.id
|
||||
for category in migration.policy.categories
|
||||
if category.name == "国产剧"
|
||||
)
|
||||
service = ClassificationExecutionService(_Runtime(migration.policy))
|
||||
source = MediaInfo(
|
||||
media_source=MediaSource.Douban,
|
||||
media_id="35593344",
|
||||
type=MediaType.TV,
|
||||
title="测试剧",
|
||||
production_countries=[{"name": "中国大陆"}],
|
||||
)
|
||||
classified = service.finalize(source)
|
||||
assert classified.classification.effective.category_id == domestic
|
||||
assert classified.classification.state == "complete"
|
||||
|
||||
classified.clear()
|
||||
supplemented = service.finalize(classified)
|
||||
|
||||
assert supplemented.classification is not None
|
||||
assert supplemented.classification.effective.category_id == domestic
|
||||
assert supplemented.classification.effective.source == "automatic"
|
||||
assert supplemented.library_category == "国产剧"
|
||||
# 本次求值确实缺事实,推荐结果和状态仍如实记录,供预览解释差异。
|
||||
assert supplemented.classification.state == "partial"
|
||||
assert supplemented.classification.recommended.source == "fallback"
|
||||
|
||||
|
||||
def test_execution_keeps_fallback_when_complete_facts_no_longer_match() -> None:
|
||||
"""事实读齐后不再命中规则时,兜底结果必须生效而不是复用旧自动分类。"""
|
||||
service = ClassificationExecutionService(_Runtime(_policy()))
|
||||
source = MediaInfo(
|
||||
media_source="douban",
|
||||
media_id="1",
|
||||
type=MediaType.MOVIE,
|
||||
origin_country=["JP"],
|
||||
genres=[{"name": "动画"}],
|
||||
)
|
||||
classified = service.finalize(source)
|
||||
assert classified.classification.effective.category_id == "movie.jp"
|
||||
|
||||
classified.origin_country = ["US"]
|
||||
reclassified = service.finalize(classified)
|
||||
|
||||
assert reclassified.classification is not None
|
||||
assert reclassified.classification.state == "complete"
|
||||
assert reclassified.classification.effective.category_id == "movie.other"
|
||||
assert reclassified.classification.effective.source == "fallback"
|
||||
assert reclassified.library_category == "其它电影"
|
||||
|
||||
|
||||
def test_execution_applies_manual_effective_override_without_losing_recommendation() -> None:
|
||||
"""订阅或目录人工覆盖只替换 effective,仍保留自动推荐供 UI 解释。"""
|
||||
source = MediaInfo(
|
||||
|
||||
Reference in New Issue
Block a user