refactor: expand chain runtime config snapshot

This commit is contained in:
jxxghp
2026-08-22 13:21:52 +08:00
parent 941c167084
commit fa2bf1fe4d
13 changed files with 78 additions and 50 deletions
+7 -1
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Protocol from typing import Any, Optional, Protocol
class SystemConfigReader(Protocol): class SystemConfigReader(Protocol):
@@ -80,6 +80,12 @@ class ChainRuntimeConfig:
"""Chain 在一次宿主生命周期内使用的基础配置快照。""" """Chain 在一次宿主生命周期内使用的基础配置快照。"""
media_extensions: tuple[str, ...] media_extensions: tuple[str, ...]
superuser: str = "admin"
media_recognize_share: bool = False
auxiliary_auth_enable: bool = False
global_image_cache: bool = False
auto_download_user: Optional[str] = None
resource_url: Optional[str] = None
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
+10 -7
View File
@@ -13,7 +13,6 @@ from app.domain.context import Context, MediaInfo, MusicInfo, TorrentInfo
from app.domain.meta.metabase import MetaBase from app.domain.meta.metabase import MetaBase
from app.foundation.identity import normalize_internal_user_id from app.foundation.identity import normalize_internal_user_id
from app.application.messaging.message import MessageTemplateHelper from app.application.messaging.message import MessageTemplateHelper
from app.runtime.config import settings
from app.runtime.extensions.service_config import ServiceConfigHelper from app.runtime.extensions.service_config import ServiceConfigHelper
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.message import MessageResponse from app.schemas.message import MessageResponse
@@ -176,7 +175,9 @@ class NotificationMixin:
# 仅发送管理员 # 仅发送管理员
logger.info(f"{send_message.mtype} 的消息已设置发送给管理员") logger.info(f"{send_message.mtype} 的消息已设置发送给管理员")
# 读取管理员消息IDS # 读取管理员消息IDS
send_message.targets = useroper.get_settings(settings.SUPERUSER) send_message.targets = useroper.get_settings(
self.runtime_config.superuser
)
admin_sended = True admin_sended = True
elif action == "user" and send_message.username: elif action == "user" and send_message.username:
# 发送对应用户 # 发送对应用户
@@ -196,7 +197,7 @@ class NotificationMixin:
) )
# 读取管理员消息IDS # 读取管理员消息IDS
send_message.targets = useroper.get_settings( send_message.targets = useroper.get_settings(
settings.SUPERUSER self.runtime_config.superuser
) )
admin_sended = True admin_sended = True
else: else:
@@ -205,7 +206,7 @@ class NotificationMixin:
f"用户 {send_message.username} 不存在,消息无法发送到对应用户" f"用户 {send_message.username} 不存在,消息无法发送到对应用户"
) )
continue continue
elif send_message.username == settings.SUPERUSER: elif send_message.username == self.runtime_config.superuser:
# 管理员同名已发送 # 管理员同名已发送
admin_sended = True admin_sended = True
else: else:
@@ -292,7 +293,9 @@ class NotificationMixin:
# 仅发送管理员 # 仅发送管理员
logger.info(f"{send_message.mtype} 的消息已设置发送给管理员") logger.info(f"{send_message.mtype} 的消息已设置发送给管理员")
# 读取管理员消息IDS # 读取管理员消息IDS
send_message.targets = useroper.get_settings(settings.SUPERUSER) send_message.targets = useroper.get_settings(
self.runtime_config.superuser
)
admin_sended = True admin_sended = True
elif action == "user" and send_message.username: elif action == "user" and send_message.username:
# 发送对应用户 # 发送对应用户
@@ -312,7 +315,7 @@ class NotificationMixin:
) )
# 读取管理员消息IDS # 读取管理员消息IDS
send_message.targets = useroper.get_settings( send_message.targets = useroper.get_settings(
settings.SUPERUSER self.runtime_config.superuser
) )
admin_sended = True admin_sended = True
else: else:
@@ -321,7 +324,7 @@ class NotificationMixin:
f"用户 {send_message.username} 不存在,消息无法发送到对应用户" f"用户 {send_message.username} 不存在,消息无法发送到对应用户"
) )
continue continue
elif send_message.username == settings.SUPERUSER: elif send_message.username == self.runtime_config.superuser:
# 管理员同名已发送 # 管理员同名已发送
admin_sended = True admin_sended = True
else: else:
+2 -4
View File
@@ -15,7 +15,6 @@ from app.domain.context import MediaInfo, MusicInfo
from app.domain.meta.metabase import MetaBase from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic from app.domain.meta.metamusic import MetaMusic
from app.runtime.cache import fresh, async_fresh from app.runtime.cache import fresh, async_fresh
from app.runtime.config import settings
from app.runtime.events import Event from app.runtime.events import Event
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.media import normalize_media_source, resolve_media_identity from app.schemas.media import normalize_media_source, resolve_media_identity
@@ -24,8 +23,8 @@ from app.schemas.types import ChainEventType, MediaSource, MediaType, SystemConf
class RecognitionMixin: class RecognitionMixin:
@staticmethod
def _can_use_media_recognize_share( def _can_use_media_recognize_share(
self,
meta: Optional[MetaBase], meta: Optional[MetaBase],
media_source: Optional[MediaSource], media_source: Optional[MediaSource],
media_id: Optional[str], media_id: Optional[str],
@@ -34,7 +33,7 @@ class RecognitionMixin:
仅在名称识别场景下使用共享识别,显式ID识别不再重复回查 仅在名称识别场景下使用共享识别,显式ID识别不再重复回查
""" """
return bool( return bool(
settings.MEDIA_RECOGNIZE_SHARE self.runtime_config.media_recognize_share
and meta and meta
and not media_source and not media_source
and not media_id and not media_id
@@ -515,4 +514,3 @@ class RecognitionMixin:
f"{plugin_info.media_source}:{plugin_info.media_id}" f"{plugin_info.media_source}:{plugin_info.media_id}"
) )
return plugin_info return plugin_info
+3 -5
View File
@@ -19,7 +19,6 @@ from app.domain import title as title_rules
from app.domain.context import Context, MediaInfo from app.domain.context import Context, MediaInfo
from app.domain.meta.metabase import MetaBase from app.domain.meta.metabase import MetaBase
from app.foundation import url as url_tools from app.foundation import url as url_tools
from app.runtime.config import settings
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.download import DownloadDirectory from app.schemas.download import DownloadDirectory
from app.schemas.file import FileURI from app.schemas.file import FileURI
@@ -1130,7 +1129,7 @@ class MediaInteractionChain(ChainBase):
source=source, source=source,
title=title, title=title,
userid=userid, userid=userid,
link=settings.MP_DOMAIN("#/resource"), link=self.runtime_config.resource_url,
buttons=buttons, buttons=buttons,
original_message_id=original_message_id, original_message_id=original_message_id,
original_chat_id=original_chat_id, original_chat_id=original_chat_id,
@@ -1533,12 +1532,11 @@ class MediaInteractionChain(ChainBase):
for sea, no_exist in season_map.items() for sea, no_exist in season_map.items()
] ]
@staticmethod def _should_auto_download(self, userid: Union[str, int]) -> bool:
def _should_auto_download(userid: Union[str, int]) -> bool:
""" """
判断当前用户是否命中自动下载名单。 判断当前用户是否命中自动下载名单。
""" """
auto_download_user = settings.AUTO_DOWNLOAD_USER auto_download_user = self.runtime_config.auto_download_user
return bool( return bool(
auto_download_user auto_download_user
and ( and (
+2 -2
View File
@@ -8,7 +8,7 @@ from app.chain.douban import DoubanChain
from app.chain.listenbrainz import ListenBrainzChain from app.chain.listenbrainz import ListenBrainzChain
from app.chain.tmdb import TmdbChain from app.chain.tmdb import TmdbChain
from app.runtime.cache import cached, fresh from app.runtime.cache import cached, fresh
from app.runtime.config import settings, global_vars from app.runtime.config import global_vars
from app.domain.context import MusicInfo from app.domain.context import MusicInfo
from app.application.image import ImageHelper from app.application.image import ImageHelper
from app.runtime.log import logger from app.runtime.log import logger
@@ -272,7 +272,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
:param datas: 数据列表 :param datas: 数据列表
:param progress_callback: 定时服务进度更新回调 :param progress_callback: 定时服务进度更新回调
""" """
if not settings.GLOBAL_IMAGE_CACHE: if not self.runtime_config.global_image_cache:
return return
total_num = len(datas) total_num = len(datas)
+2 -3
View File
@@ -3,7 +3,6 @@ from dataclasses import dataclass
from typing import Any, Literal, Optional, Tuple, Union from typing import Any, Literal, Optional, Tuple, Union
from app.chain import ChainBase from app.chain import ChainBase
from app.runtime.config import settings
from app.application.security.token import get_password_hash, verify_password from app.application.security.token import get_password_hash, verify_password
from app.application.chain.data import UserPortProxy as UserOper from app.application.chain.data import UserPortProxy as UserOper
from app.runtime.log import logger from app.runtime.log import logger
@@ -73,7 +72,7 @@ class UserChain(ChainBase):
return True, user_or_message return True, user_or_message
else: else:
# 用户不存在或密码错误,考虑辅助认证 # 用户不存在或密码错误,考虑辅助认证
if settings.AUXILIARY_AUTH_ENABLE: if self.runtime_config.auxiliary_auth_enable:
logger.warning("密码认证失败,尝试通过外部服务进行辅助认证 ...") logger.warning("密码认证失败,尝试通过外部服务进行辅助认证 ...")
aux_success, aux_user_or_message = self.auxiliary_authenticate(credentials=credentials) aux_success, aux_user_or_message = self.auxiliary_authenticate(credentials=credentials)
if aux_success: if aux_success:
@@ -91,7 +90,7 @@ class UserChain(ChainBase):
return False, PASSWORD_INVALID_CREDENTIALS_MESSAGE return False, PASSWORD_INVALID_CREDENTIALS_MESSAGE
elif credentials.grant_type == "authorization_code": elif credentials.grant_type == "authorization_code":
# 处理其他认证类型的分支 # 处理其他认证类型的分支
if settings.AUXILIARY_AUTH_ENABLE: if self.runtime_config.auxiliary_auth_enable:
aux_success, aux_user_or_message = self.auxiliary_authenticate(credentials=credentials) aux_success, aux_user_or_message = self.auxiliary_authenticate(credentials=credentials)
if aux_success: if aux_success:
return True, aux_user_or_message return True, aux_user_or_message
+8 -2
View File
@@ -207,14 +207,20 @@ def _build_scheduler_runtime_config() -> SchedulerRuntimeConfig:
def _build_chain_runtime_config() -> ChainRuntimeConfig: def _build_chain_runtime_config() -> ChainRuntimeConfig:
"""构建 Chain 通用媒体文件后缀配置快照。""" """构建 Chain 在本次实例生命周期内使用的部署配置快照。"""
return ChainRuntimeConfig( return ChainRuntimeConfig(
media_extensions=tuple( media_extensions=tuple(
settings.RMT_MEDIAEXT settings.RMT_MEDIAEXT
+ settings.DOWNLOAD_TMPEXT + settings.DOWNLOAD_TMPEXT
+ settings.RMT_SUBEXT + settings.RMT_SUBEXT
+ settings.RMT_AUDIOEXT + settings.RMT_AUDIOEXT
) ),
superuser=settings.SUPERUSER,
media_recognize_share=settings.MEDIA_RECOGNIZE_SHARE,
auxiliary_auth_enable=settings.AUXILIARY_AUTH_ENABLE,
global_image_cache=settings.GLOBAL_IMAGE_CACHE,
auto_download_user=settings.AUTO_DOWNLOAD_USER,
resource_url=settings.MP_DOMAIN("#/resource"),
) )
@@ -567,6 +567,9 @@ app/api/dependencies/ # 按领域拆分依赖工厂
- 登录、仪表板和整理历史 API 不再直接导入 `settings``Scheduler` 已清除全部直接 `settings` 访问, - 登录、仪表板和整理历史 API 不再直接导入 `settings``Scheduler` 已清除全部直接 `settings` 访问,
用户认证配置改走 `SystemConfigService``StorageChain` 的媒体后缀改走 Chain snapshot。canonical 配置债务 用户认证配置改走 `SystemConfigService``StorageChain` 的媒体后缀改走 Chain snapshot。canonical 配置债务
从 169/15 降到 164 个 settings import 文件/14 个 SystemConfigOper 构造点。 从 169/15 降到 164 个 settings import 文件/14 个 SystemConfigOper 构造点。
- Chain snapshot 继续覆盖超级用户、共享识别、辅助认证、全局图片缓存、自动下载用户和资源页链接;
消息、识别、交互、推荐和用户链的 5 个直接 `settings` 导入被移除,当前低水位进一步降到
161 个 settings import 文件,插件 SDK 与兼容入口未改。
- 直接调用 endpoint 和显式构造 `ChainRuntimeContext` 的旧测试/兼容入口仍有 fallback;正式 FastAPI 与 - 直接调用 endpoint 和显式构造 `ChainRuntimeContext` 的旧测试/兼容入口仍有 fallback;正式 FastAPI 与
Startup 路径始终使用 HostRuntime 注入。插件 SDK 的 `app.sdk.config.settings`、动态 API 返回和事件字段未改。 Startup 路径始终使用 HostRuntime 注入。插件 SDK 的 `app.sdk.config.settings`、动态 API 返回和事件字段未改。
@@ -9,7 +9,7 @@
"root": "app" "root": "app"
}, },
"settings_imports": { "settings_imports": {
"count": 166, "count": 161,
"files": [ "files": [
"app/adapters/cache/backends.py", "app/adapters/cache/backends.py",
"app/adapters/cache/redis.py", "app/adapters/cache/redis.py",
@@ -69,14 +69,10 @@
"app/application/security/token.py", "app/application/security/token.py",
"app/application/security/url.py", "app/application/security/url.py",
"app/application/torrent.py", "app/application/torrent.py",
"app/chain/_messaging.py",
"app/chain/_recognition.py",
"app/chain/_transfer.py", "app/chain/_transfer.py",
"app/chain/download.py", "app/chain/download.py",
"app/chain/interaction.py",
"app/chain/media.py", "app/chain/media.py",
"app/chain/message.py", "app/chain/message.py",
"app/chain/recommend.py",
"app/chain/scraping.py", "app/chain/scraping.py",
"app/chain/search.py", "app/chain/search.py",
"app/chain/site.py", "app/chain/site.py",
@@ -84,7 +80,6 @@
"app/chain/system.py", "app/chain/system.py",
"app/chain/torrents.py", "app/chain/torrents.py",
"app/chain/transfer.py", "app/chain/transfer.py",
"app/chain/user.py",
"app/cli.py", "app/cli.py",
"app/db/base.py", "app/db/base.py",
"app/db/engine.py", "app/db/engine.py",
+2 -6
View File
@@ -13,8 +13,8 @@
"runtime_to_db": [], "runtime_to_db": [],
"workflow_to_db": [] "workflow_to_db": []
}, },
"edge_count": 6364, "edge_count": 6360,
"edge_sha256": "0f241a46ed27309c2071d4c99f00ba067ad02b3974581cc206ddec4c522a982c", "edge_sha256": "09c923d8b167a889e22829c320e05f8801c62400f672b1386d6524c55b064564",
"edges": [ "edges": [
"app -> app.runtime", "app -> app.runtime",
"app -> app.runtime.compat", "app -> app.runtime.compat",
@@ -2851,7 +2851,6 @@
"app.chain._messaging -> app.foundation", "app.chain._messaging -> app.foundation",
"app.chain._messaging -> app.foundation.identity", "app.chain._messaging -> app.foundation.identity",
"app.chain._messaging -> app.runtime", "app.chain._messaging -> app.runtime",
"app.chain._messaging -> app.runtime.config",
"app.chain._messaging -> app.runtime.extensions", "app.chain._messaging -> app.runtime.extensions",
"app.chain._messaging -> app.runtime.extensions.service_config", "app.chain._messaging -> app.runtime.extensions.service_config",
"app.chain._messaging -> app.runtime.log", "app.chain._messaging -> app.runtime.log",
@@ -2891,7 +2890,6 @@
"app.chain._recognition -> app.domain.meta.metamusic", "app.chain._recognition -> app.domain.meta.metamusic",
"app.chain._recognition -> app.runtime", "app.chain._recognition -> app.runtime",
"app.chain._recognition -> app.runtime.cache", "app.chain._recognition -> app.runtime.cache",
"app.chain._recognition -> app.runtime.config",
"app.chain._recognition -> app.runtime.events", "app.chain._recognition -> app.runtime.events",
"app.chain._recognition -> app.runtime.log", "app.chain._recognition -> app.runtime.log",
"app.chain._recognition -> app.schemas", "app.chain._recognition -> app.schemas",
@@ -3015,7 +3013,6 @@
"app.chain.interaction -> app.foundation", "app.chain.interaction -> app.foundation",
"app.chain.interaction -> app.foundation.url", "app.chain.interaction -> app.foundation.url",
"app.chain.interaction -> app.runtime", "app.chain.interaction -> app.runtime",
"app.chain.interaction -> app.runtime.config",
"app.chain.interaction -> app.runtime.log", "app.chain.interaction -> app.runtime.log",
"app.chain.interaction -> app.schemas", "app.chain.interaction -> app.schemas",
"app.chain.interaction -> app.schemas.download", "app.chain.interaction -> app.schemas.download",
@@ -3366,7 +3363,6 @@
"app.chain.user -> app.application.security.token", "app.chain.user -> app.application.security.token",
"app.chain.user -> app.chain", "app.chain.user -> app.chain",
"app.chain.user -> app.runtime", "app.chain.user -> app.runtime",
"app.chain.user -> app.runtime.config",
"app.chain.user -> app.runtime.log", "app.chain.user -> app.runtime.log",
"app.chain.user -> app.schemas", "app.chain.user -> app.schemas",
"app.chain.user -> app.schemas.event", "app.chain.user -> app.schemas.event",
+19 -2
View File
@@ -1,8 +1,11 @@
"""配置快照与窄读写端口测试。""" """配置快照与窄读写端口测试。"""
import asyncio import asyncio
from dataclasses import FrozenInstanceError
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
import pytest
from app.application.configuration import ( from app.application.configuration import (
ApiRuntimeConfig, ApiRuntimeConfig,
ChainRuntimeConfig, ChainRuntimeConfig,
@@ -78,5 +81,19 @@ def test_api_runtime_provider_returns_frozen_snapshot_per_request() -> None:
assert before_reload.ai_agent_enable is False assert before_reload.ai_agent_enable is False
assert after_reload.ai_agent_enable is True assert after_reload.ai_agent_enable is True
configure_runtime_configuration,
get_api_runtime_config_snapshot,
def test_chain_runtime_config_is_an_instance_scoped_frozen_snapshot() -> None:
"""Chain 配置应随实例固定,避免同一业务调用中途读取到 reload 后的新值。"""
snapshot = ChainRuntimeConfig(
media_extensions=(".mkv",),
superuser="root",
media_recognize_share=True,
resource_url="https://example.test/#/resource",
)
assert snapshot.superuser == "root"
assert snapshot.media_recognize_share is True
assert snapshot.resource_url == "https://example.test/#/resource"
with pytest.raises(FrozenInstanceError):
snapshot.superuser = "changed" # type: ignore[misc]
+18 -9
View File
@@ -4,6 +4,7 @@
共享识别成功后回填本地缓存音乐识别上报/查询载荷以及命中缓存不重复上报等场景 共享识别成功后回填本地缓存音乐识别上报/查询载荷以及命中缓存不重复上报等场景
""" """
import asyncio import asyncio
from dataclasses import replace
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
from app.chain import ChainBase from app.chain import ChainBase
@@ -41,6 +42,14 @@ def _tmdb_media(
) )
def _enable_media_recognize_share(chain: ChainBase) -> None:
"""为单个链实例启用共享识别配置快照。"""
chain.runtime_config = replace(
chain.runtime_config,
media_recognize_share=True,
)
def test_report_shared_result_after_local_recognize_success(): def test_report_shared_result_after_local_recognize_success():
"""本地识别成功后应上报共享识别结果。""" """本地识别成功后应上报共享识别结果。"""
chain = ChainBase() chain = ChainBase()
@@ -64,6 +73,7 @@ def test_report_shared_result_after_local_recognize_success():
def test_query_shared_result_when_local_recognize_failed(): def test_query_shared_result_when_local_recognize_failed():
"""本地识别失败后应回查共享识别结果,并按共享ID再次识别。""" """本地识别失败后应回查共享识别结果,并按共享ID再次识别。"""
chain = ChainBase() chain = ChainBase()
_enable_media_recognize_share(chain)
meta = _build_meta("测试剧集") meta = _build_meta("测试剧集")
shared_media = _tmdb_media("测试剧集", 200, MediaType.TV, year="2024") shared_media = _tmdb_media("测试剧集", 200, MediaType.TV, year="2024")
@@ -109,6 +119,7 @@ def test_query_shared_result_when_local_recognize_failed():
def test_async_query_shared_result_when_local_recognize_failed(): def test_async_query_shared_result_when_local_recognize_failed():
"""异步识别失败后也应回查共享识别结果。""" """异步识别失败后也应回查共享识别结果。"""
chain = ChainBase() chain = ChainBase()
_enable_media_recognize_share(chain)
meta = _build_meta("测试异步剧集") meta = _build_meta("测试异步剧集")
shared_media = _tmdb_media("测试异步剧集", 300, MediaType.TV, year="2025") shared_media = _tmdb_media("测试异步剧集", 300, MediaType.TV, year="2025")
async_run_module = AsyncMock(side_effect=[None, shared_media]) async_run_module = AsyncMock(side_effect=[None, shared_media])
@@ -157,6 +168,7 @@ def test_async_query_shared_result_when_local_recognize_failed():
def test_backfill_local_cache_after_shared_recognize_success(): def test_backfill_local_cache_after_shared_recognize_success():
"""共享识别后二次本地识别成功时,应回填原始名称对应的本地识别缓存。""" """共享识别后二次本地识别成功时,应回填原始名称对应的本地识别缓存。"""
chain = ChainBase() chain = ChainBase()
_enable_media_recognize_share(chain)
meta = _build_meta("测试缓存回填", MediaType.MOVIE) meta = _build_meta("测试缓存回填", MediaType.MOVIE)
shared_media = MediaInfo( shared_media = MediaInfo(
media_source=MediaSource.TMDB, media_source=MediaSource.TMDB,
@@ -304,6 +316,7 @@ def test_report_shared_result_with_distinct_keyword_meta():
def test_query_shared_result_with_distinct_keyword_meta(): def test_query_shared_result_with_distinct_keyword_meta():
"""本地识别失败后应按辅助前名称回查共享结果。""" """本地识别失败后应按辅助前名称回查共享结果。"""
chain = ChainBase() chain = ChainBase()
_enable_media_recognize_share(chain)
meta = _build_meta("辅助识别后的名称", MediaType.TV) meta = _build_meta("辅助识别后的名称", MediaType.TV)
meta.year = "2024" meta.year = "2024"
share_meta = _build_meta("辅助识别前的名称", MediaType.UNKNOWN) share_meta = _build_meta("辅助识别前的名称", MediaType.UNKNOWN)
@@ -594,6 +607,7 @@ def test_chain_recognize_media_reports_music_share_result():
def test_chain_recognize_media_queries_music_share_when_local_failed(): def test_chain_recognize_media_queries_music_share_when_local_failed():
"""音乐本地识别失败后应回查共享识别并按数据源原生 ID 二次识别。""" """音乐本地识别失败后应回查共享识别并按数据源原生 ID 二次识别。"""
chain = MediaChain() chain = MediaChain()
_enable_media_recognize_share(chain)
meta = MetaMusic(title="晴天", artists=["周杰伦"]) meta = MetaMusic(title="晴天", artists=["周杰伦"])
music = _music_info() music = _music_info()
@@ -641,6 +655,7 @@ def test_chain_recognize_media_queries_music_share_when_local_failed():
def test_chain_recognize_media_queries_music_share_after_local_fallback(): def test_chain_recognize_media_queries_music_share_after_local_fallback():
"""本地标签兜底没有远端身份时,仍应通过共享结果补成标准音乐身份。""" """本地标签兜底没有远端身份时,仍应通过共享结果补成标准音乐身份。"""
chain = MediaChain() chain = MediaChain()
_enable_media_recognize_share(chain)
meta = MetaMusic(title="晴天", artists=["周杰伦"]) meta = MetaMusic(title="晴天", artists=["周杰伦"])
fallback = MusicInfo(title="晴天", artists=["周杰伦"]) fallback = MusicInfo(title="晴天", artists=["周杰伦"])
music = _music_info() music = _music_info()
@@ -669,9 +684,6 @@ def test_chain_recognize_media_queries_music_share_after_local_fallback():
), patch.object( ), patch.object(
chain, chain,
"_update_local_recognize_cache", "_update_local_recognize_cache",
), patch(
"app.chain._recognition.settings.MEDIA_RECOGNIZE_SHARE",
True,
): ):
result = chain.recognize_media(meta=meta, cache=False) result = chain.recognize_media(meta=meta, cache=False)
@@ -687,6 +699,7 @@ def test_chain_recognize_media_queries_music_share_after_local_fallback():
def test_chain_async_recognize_media_queries_music_share_after_local_fallback(): def test_chain_async_recognize_media_queries_music_share_after_local_fallback():
"""异步音乐识别也必须在返回本地兜底前尝试共享身份补全。""" """异步音乐识别也必须在返回本地兜底前尝试共享身份补全。"""
chain = MediaChain() chain = MediaChain()
_enable_media_recognize_share(chain)
meta = MetaMusic(title="晴天", artists=["周杰伦"]) meta = MetaMusic(title="晴天", artists=["周杰伦"])
fallback = MusicInfo(title="晴天", artists=["周杰伦"]) fallback = MusicInfo(title="晴天", artists=["周杰伦"])
music = _music_info() music = _music_info()
@@ -717,9 +730,6 @@ def test_chain_async_recognize_media_queries_music_share_after_local_fallback():
chain, chain,
"_async_update_local_recognize_cache", "_async_update_local_recognize_cache",
new=AsyncMock(), new=AsyncMock(),
), patch(
"app.chain._recognition.settings.MEDIA_RECOGNIZE_SHARE",
True,
): ):
result = await chain.async_recognize_media(meta=meta, cache=False) result = await chain.async_recognize_media(meta=meta, cache=False)
return result, query_share, recognize_source return result, query_share, recognize_source
@@ -745,6 +755,7 @@ def test_chain_async_recognize_media_queries_music_share_after_local_fallback():
def test_chain_recognize_media_skips_music_report_for_fallback_result(): def test_chain_recognize_media_skips_music_report_for_fallback_result():
"""共享也未命中时保留音乐标签兜底,且不把无身份结果上报。""" """共享也未命中时保留音乐标签兜底,且不把无身份结果上报。"""
chain = MediaChain() chain = MediaChain()
_enable_media_recognize_share(chain)
meta = MetaMusic(title="未知曲目", artists=["未知艺术家"]) meta = MetaMusic(title="未知曲目", artists=["未知艺术家"])
fallback = MusicInfo(title="未知曲目", artists=["未知艺术家"]) fallback = MusicInfo(title="未知曲目", artists=["未知艺术家"])
@@ -753,9 +764,7 @@ def test_chain_recognize_media_skips_music_report_for_fallback_result():
return_value=None, return_value=None,
) as query_mock, patch( ) as query_mock, patch(
"app.chain._recognition.MoviePilotServerHelper.report_recognize_share" "app.chain._recognition.MoviePilotServerHelper.report_recognize_share"
) as report_mock, patch( ) as report_mock:
"app.chain._recognition.settings.MEDIA_RECOGNIZE_SHARE", True
):
result = chain.recognize_media(meta=meta, cache=False) result = chain.recognize_media(meta=meta, cache=False)
assert result is fallback assert result is fallback
@@ -41,6 +41,7 @@ def _bare_chain() -> ChainBase:
"""构造不执行初始化的识别链实例,并挂上无插件响应的事件管理器桩。""" """构造不执行初始化的识别链实例,并挂上无插件响应的事件管理器桩。"""
chain = object.__new__(ChainBase) chain = object.__new__(ChainBase)
chain.eventmanager = Mock(check=Mock(return_value=False)) chain.eventmanager = Mock(check=Mock(return_value=False))
chain.runtime_config = SimpleNamespace(media_recognize_share=True)
return chain return chain
@@ -57,7 +58,6 @@ def test_sync_shared_recognize_success_increments_persisted_count(monkeypatch):
type=MediaType.MOVIE, type=MediaType.MOVIE,
) )
increment = _mock_counter(monkeypatch) increment = _mock_counter(monkeypatch)
monkeypatch.setattr("app.chain._recognition.settings.MEDIA_RECOGNIZE_SHARE", True)
monkeypatch.setattr(chain, "run_module", Mock(side_effect=[None, media])) monkeypatch.setattr(chain, "run_module", Mock(side_effect=[None, media]))
monkeypatch.setattr(chain, "_update_local_recognize_cache", Mock()) monkeypatch.setattr(chain, "_update_local_recognize_cache", Mock())
monkeypatch.setattr( monkeypatch.setattr(
@@ -86,7 +86,6 @@ def test_sync_shared_result_without_local_match_does_not_increment(monkeypatch):
chain = _bare_chain() chain = _bare_chain()
meta = _build_meta("共享识别失败电影") meta = _build_meta("共享识别失败电影")
increment = _mock_counter(monkeypatch) increment = _mock_counter(monkeypatch)
monkeypatch.setattr("app.chain._recognition.settings.MEDIA_RECOGNIZE_SHARE", True)
monkeypatch.setattr(chain, "run_module", Mock(side_effect=[None, None])) monkeypatch.setattr(chain, "run_module", Mock(side_effect=[None, None]))
monkeypatch.setattr( monkeypatch.setattr(
MoviePilotServerHelper, MoviePilotServerHelper,
@@ -122,7 +121,6 @@ def test_async_shared_recognize_success_increments_persisted_count(monkeypatch):
type=MediaType.MOVIE, type=MediaType.MOVIE,
) )
increment = _mock_counter(monkeypatch) increment = _mock_counter(monkeypatch)
monkeypatch.setattr("app.chain._recognition.settings.MEDIA_RECOGNIZE_SHARE", True)
monkeypatch.setattr( monkeypatch.setattr(
chain, chain,
"async_run_module", "async_run_module",