refactor: use injected eventmanager in chain instance methods

This commit is contained in:
jxxghp
2026-08-25 06:59:18 +08:00
parent 9c44eeb493
commit 1f076eba04
8 changed files with 108 additions and 69 deletions
+1 -1
View File
@@ -961,7 +961,7 @@ class DownloadChain(ChainBase):
downloader=downloader, downloader=downloader,
origin=source, origin=source,
) )
event = eventmanager.send_event(ChainEventType.ResourceSelection, event_data) event = self.eventmanager.send_event(ChainEventType.ResourceSelection, event_data)
if event and event.event_data: if event and event.event_data:
event_data = event.event_data event_data = event.event_data
if event_data.updated and event_data.updated_contexts is not None: if event_data.updated and event_data.updated_contexts is not None:
+13 -11
View File
@@ -19,7 +19,7 @@ from app.domain.context import (
MusicArtistInfo, MusicArtistInfo,
MusicInfo, MusicInfo,
) )
from app.runtime.events import eventmanager, Event from app.runtime.events import Event
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.domain.metainfo import MetaInfo, MetaInfoPath from app.domain.metainfo import MetaInfo, MetaInfoPath
@@ -450,8 +450,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
@staticmethod
def select_recognize_source( def select_recognize_source(
self,
log_name: str, log_context: str, native_fn, plugin_fn, log_name: str, log_context: str, native_fn, plugin_fn,
is_recognized=None, is_recognized=None,
plugin_event: ChainEventType = ChainEventType.NameRecognize, plugin_event: ChainEventType = ChainEventType.NameRecognize,
@@ -470,7 +470,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
if is_recognized is None: if is_recognized is None:
is_recognized = lambda result: bool(result) is_recognized = lambda result: bool(result)
mediainfo = None mediainfo = None
plugin_available = eventmanager.check(plugin_event) # 插件可用性检查走注入的事件管理器,避免链实例直连全局单例
plugin_available = self.eventmanager.check(plugin_event)
if get_chain_runtime_config_snapshot().recognize_plugin_first and plugin_available: if get_chain_runtime_config_snapshot().recognize_plugin_first and plugin_available:
# 插件优先 # 插件优先
logger.info(f"插件识别优先模式已开启。请求辅助识别,标题:{log_name} ...") logger.info(f"插件识别优先模式已开启。请求辅助识别,标题:{log_name} ...")
@@ -758,7 +759,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
music_type=music_type, music_type=music_type,
) )
# 发送请求事件,等待结果 # 发送请求事件,等待结果
result: Event = eventmanager.send_event( result: Event = self.eventmanager.send_event(
ChainEventType.NameRecognize, ChainEventType.NameRecognize,
{ {
"title": title, "title": title,
@@ -820,7 +821,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
:param music_type: 音乐实体类型 :param music_type: 音乐实体类型
""" """
# 发送音乐名称识别事件,等待插件返回标题要素 # 发送音乐名称识别事件,等待插件返回标题要素
result: Event = eventmanager.send_event( result: Event = self.eventmanager.send_event(
ChainEventType.MusicNameRecognize, ChainEventType.MusicNameRecognize,
{ {
"title": title, "title": title,
@@ -1507,14 +1508,14 @@ class MediaChain(ChainBase, metaclass=Singleton):
media_id=media_id, media_id=media_id,
target_media_source=target_source, target_media_source=target_source,
) )
event = eventmanager.send_event( event = self.eventmanager.send_event(
ChainEventType.MediaRecognizeConvert, event_data, ChainEventType.MediaRecognizeConvert, event_data,
) )
return event_data.media_dict if event and event_data.media_dict else None return event_data.media_dict if event and event_data.media_dict else None
@staticmethod
async def async_select_recognize_source( async def async_select_recognize_source(
self,
log_name: str, log_context: str, native_fn, plugin_fn, log_name: str, log_context: str, native_fn, plugin_fn,
is_recognized=None, is_recognized=None,
plugin_event: ChainEventType = ChainEventType.NameRecognize, plugin_event: ChainEventType = ChainEventType.NameRecognize,
@@ -1532,7 +1533,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
if is_recognized is None: if is_recognized is None:
is_recognized = lambda result: bool(result) is_recognized = lambda result: bool(result)
mediainfo = None mediainfo = None
plugin_available = eventmanager.check(plugin_event) # 插件可用性检查走注入的事件管理器,避免链实例直连全局单例
plugin_available = self.eventmanager.check(plugin_event)
if get_chain_runtime_config_snapshot().recognize_plugin_first and plugin_available: if get_chain_runtime_config_snapshot().recognize_plugin_first and plugin_available:
# 插件优先 # 插件优先
logger.info(f"插件优先模式已开启。请求辅助识别,标题:{log_name} ...") logger.info(f"插件优先模式已开启。请求辅助识别,标题:{log_name} ...")
@@ -1696,7 +1698,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
music_type=music_type, music_type=music_type,
) )
# 发送请求事件,等待结果 # 发送请求事件,等待结果
result: Event = await eventmanager.async_send_event( result: Event = await self.eventmanager.async_send_event(
ChainEventType.NameRecognize, ChainEventType.NameRecognize,
{ {
"title": title, "title": title,
@@ -1758,7 +1760,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
:param music_type: 音乐实体类型 :param music_type: 音乐实体类型
""" """
# 发送音乐名称识别事件,等待插件返回标题要素 # 发送音乐名称识别事件,等待插件返回标题要素
result: Event = await eventmanager.async_send_event( result: Event = await self.eventmanager.async_send_event(
ChainEventType.MusicNameRecognize, ChainEventType.MusicNameRecognize,
{ {
"title": title, "title": title,
@@ -2053,7 +2055,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
media_id=media_id, media_id=media_id,
target_media_source=target_source, target_media_source=target_source,
) )
event = await eventmanager.async_send_event( event = await self.eventmanager.async_send_event(
ChainEventType.MediaRecognizeConvert, event_data, ChainEventType.MediaRecognizeConvert, event_data,
) )
return event_data.media_dict if event and event_data.media_dict else None return event_data.media_dict if event and event_data.media_dict else None
+3 -3
View File
@@ -74,7 +74,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
payload=userdata.model_dump(), payload=userdata.model_dump(),
) )
# 发送事件 # 发送事件
eventmanager.send_event(EventType.SiteRefreshed, { self.eventmanager.send_event(EventType.SiteRefreshed, {
"site_id": site.get("id") "site_id": site.get("id")
}) })
self._post_site_messages(site=site, userdata=userdata) self._post_site_messages(site=site, userdata=userdata)
@@ -163,7 +163,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
data={"total": total_num, "finished": index}, data={"total": total_num, "finished": index},
) )
if any_site_updated: if any_site_updated:
eventmanager.send_event(EventType.SiteRefreshed, { self.eventmanager.send_event(EventType.SiteRefreshed, {
"site_id": "*" "site_id": "*"
}) })
if progress_callback: if progress_callback:
@@ -461,7 +461,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
# 通知站点更新 # 通知站点更新
if indexer: if indexer:
eventmanager.send_event(EventType.SiteUpdated, { self.eventmanager.send_event(EventType.SiteUpdated, {
"domain": domain, "domain": domain,
}) })
if progress_callback: if progress_callback:
+3 -3
View File
@@ -934,7 +934,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
"""同步执行提交后消息、事件和统计,异常不再触碰数据库事务。""" """同步执行提交后消息、事件和统计,异常不再触碰数据库事务。"""
if context.notification: if context.notification:
self.post_message(_SchemaMessage.model_validate(context.notification)) self.post_message(_SchemaMessage.model_validate(context.notification))
eventmanager.send_event(EventType.SubscribeAdded, { self.eventmanager.send_event(EventType.SubscribeAdded, {
"subscribe_id": subscribe_id, "subscribe_id": subscribe_id,
"idempotency_key": ( "idempotency_key": (
f"subscribe.added:{subscribe_id}:" f"subscribe.added:{subscribe_id}:"
@@ -958,7 +958,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
await self.async_post_message( await self.async_post_message(
_SchemaMessage.model_validate(context.notification) _SchemaMessage.model_validate(context.notification)
) )
await eventmanager.async_send_event(EventType.SubscribeAdded, { await self.eventmanager.async_send_event(EventType.SubscribeAdded, {
"subscribe_id": subscribe_id, "subscribe_id": subscribe_id,
"idempotency_key": ( "idempotency_key": (
f"subscribe.added:{subscribe_id}:" f"subscribe.added:{subscribe_id}:"
@@ -3112,7 +3112,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
if subscribe.state == "P": if subscribe.state == "P":
return return
# 发送订阅完成判定事件,在写入 DB 前,允许外部据完结策略否决本次自动完成 # 发送订阅完成判定事件,在写入 DB 前,允许外部据完结策略否决本次自动完成
completion_event = eventmanager.send_event( completion_event = self.eventmanager.send_event(
ChainEventType.SubscribeCompletionCheck, ChainEventType.SubscribeCompletionCheck,
SubscribeCompletionCheckEventData(subscribe=subscribe, mediainfo=mediainfo, meta=meta)) SubscribeCompletionCheckEventData(subscribe=subscribe, mediainfo=mediainfo, meta=meta))
if completion_event and completion_event.event_data: if completion_event and completion_event.event_data:
+2 -3
View File
@@ -18,7 +18,6 @@ from app.chain.storage import StorageChain
from app.chain.tmdb import TmdbChain from app.chain.tmdb import TmdbChain
from app.runtime.config import global_vars from app.runtime.config import global_vars
from app.domain.context import MediaInfo, MusicInfo, TorrentInfo from app.domain.context import MediaInfo, MusicInfo, TorrentInfo
from app.runtime.events import eventmanager
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.domain.metainfo import MetaInfoPath from app.domain.metainfo import MetaInfoPath
@@ -1487,7 +1486,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
source_event_data = StorageOperSelectionEventData( source_event_data = StorageOperSelectionEventData(
storage=task.fileitem.storage, storage=task.fileitem.storage,
) )
source_event = eventmanager.send_event( source_event = self.eventmanager.send_event(
ChainEventType.StorageOperSelection, source_event_data ChainEventType.StorageOperSelection, source_event_data
) )
# 使用事件返回的上下文数据 # 使用事件返回的上下文数据
@@ -1503,7 +1502,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
target_event_data = StorageOperSelectionEventData( target_event_data = StorageOperSelectionEventData(
storage=task.target_storage, storage=task.target_storage,
) )
target_event = eventmanager.send_event( target_event = self.eventmanager.send_event(
ChainEventType.StorageOperSelection, target_event_data ChainEventType.StorageOperSelection, target_event_data
) )
# 使用事件返回的上下文数据 # 使用事件返回的上下文数据
+24 -24
View File
@@ -108,13 +108,14 @@ def test_media_identity_conversion_rejects_invalid_pair_without_plugin_handler()
media_source=MediaSource.Bangumi, media_source=MediaSource.Bangumi,
media_id="0", media_id="0",
) is None ) is None
with patch("app.chain.media.eventmanager.send_event", return_value=None): # 无插件处理器:注入返回空的 eventmanager 桩
assert MediaChain.convert_media_identity( chain.eventmanager = Mock(send_event=Mock(return_value=None))
chain, assert MediaChain.convert_media_identity(
target_source=MediaSource.TheAudioDB, chain,
media_source=MediaSource.Bangumi, target_source=MediaSource.TheAudioDB,
media_id="1", media_source=MediaSource.Bangumi,
) is None media_id="1",
) is None
assert chain.tmdb_mtype is None assert chain.tmdb_mtype is None
assert chain.douban_mtype is None assert chain.douban_mtype is None
@@ -129,13 +130,14 @@ def test_media_identity_conversion_dispatches_plugin_source() -> None:
event_data.media_dict.update(result) event_data.media_dict.update(result)
return Mock(event_data=event_data) return Mock(event_data=event_data)
with patch("app.chain.media.eventmanager.send_event", side_effect=handle_event): # 事件经注入的 eventmanager 发送,测试桩直接提供实例依赖
converted = MediaChain.convert_media_identity( chain.eventmanager = Mock(send_event=Mock(side_effect=handle_event))
chain, converted = MediaChain.convert_media_identity(
target_source=MediaSource.TMDB, chain,
media_source=MediaSource("acme.video"), target_source=MediaSource.TMDB,
media_id="custom-1", media_source=MediaSource("acme.video"),
) media_id="custom-1",
)
assert converted == result assert converted == result
@@ -211,15 +213,13 @@ def test_async_media_identity_conversion_dispatches_plugin_source() -> None:
event_data.media_dict.update(result) event_data.media_dict.update(result)
return Mock(event_data=event_data) return Mock(event_data=event_data)
with patch( # 事件经注入的 eventmanager 发送,测试桩直接提供实例依赖
"app.chain.media.eventmanager.async_send_event", chain.eventmanager = Mock(async_send_event=AsyncMock(side_effect=handle_event))
new=AsyncMock(side_effect=handle_event), converted = asyncio.run(MediaChain.async_convert_media_identity(
): chain,
converted = asyncio.run(MediaChain.async_convert_media_identity( target_source=MediaSource.Douban,
chain, media_source=MediaSource("acme.video"),
target_source=MediaSource.Douban, media_id="custom-1",
media_source=MediaSource("acme.video"), ))
media_id="custom-1",
))
assert converted == result assert converted == result
+30
View File
@@ -558,6 +558,8 @@ def test_batch_download_rejects_complete_coverage_when_files_do_not_cover_target
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
chain.download_torrent = MagicMock(return_value=(b"torrent-content", "", ["demo.mkv"])) chain.download_torrent = MagicMock(return_value=(b"torrent-content", "", ["demo.mkv"]))
chain.download_single = MagicMock(return_value="hash") chain.download_single = MagicMock(return_value="hash")
@@ -588,6 +590,8 @@ def test_batch_download_preserves_special_season_zero(monkeypatch):
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
chain.download_torrent = MagicMock(return_value=(b"torrent-content", "", ["demo.mkv"])) chain.download_torrent = MagicMock(return_value=(b"torrent-content", "", ["demo.mkv"]))
chain.download_single = MagicMock(return_value="hash") chain.download_single = MagicMock(return_value="hash")
@@ -623,6 +627,8 @@ def test_batch_download_rejects_complete_coverage_when_only_missing_episodes_mat
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
chain.download_torrent = MagicMock(return_value=(b"torrent-content", "", ["demo.mkv"])) chain.download_torrent = MagicMock(return_value=(b"torrent-content", "", ["demo.mkv"]))
chain.download_single = MagicMock(return_value="hash") chain.download_single = MagicMock(return_value="hash")
@@ -655,6 +661,8 @@ def test_batch_download_tries_next_episode_candidate_when_first_download_fails(m
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
chain.download_single = MagicMock(side_effect=[None, "hash"]) chain.download_single = MagicMock(side_effect=[None, "hash"])
first_context = _build_tv_context(episode_list=[1]) first_context = _build_tv_context(episode_list=[1])
@@ -692,6 +700,8 @@ def test_batch_download_does_not_download_duplicate_movie_after_success(monkeypa
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
chain.download_single = MagicMock(return_value="hash") chain.download_single = MagicMock(return_value="hash")
first_context = SimpleNamespace( first_context = SimpleNamespace(
@@ -720,6 +730,8 @@ def test_batch_download_threads_custom_words_to_download_single(monkeypatch):
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
chain.download_single = MagicMock(return_value="hash") chain.download_single = MagicMock(return_value="hash")
context = SimpleNamespace( context = SimpleNamespace(
@@ -744,6 +756,8 @@ def test_batch_download_applies_custom_words_to_torrent_file_episodes(monkeypatc
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
chain.download_torrent = MagicMock( chain.download_torrent = MagicMock(
return_value=(b"torrent-content", "", ["A.Will.Eternal.S04E05.mkv"]), return_value=(b"torrent-content", "", ["A.Will.Eternal.S04E05.mkv"]),
) )
@@ -807,6 +821,8 @@ def test_download_single_records_failure_cooldown_when_downloader_rejects(monkey
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
error_msg = "添加种子任务失败:无法读取种子文件" error_msg = "添加种子任务失败:无法读取种子文件"
chain.download = MagicMock(return_value=("qb", None, "Original", error_msg)) chain.download = MagicMock(return_value=("qb", None, "Original", error_msg))
chain.post_message = MagicMock() chain.post_message = MagicMock()
@@ -943,6 +959,8 @@ def test_batch_download_skips_failed_subscription_resource_and_tries_next(monkey
) )
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
chain.download_single = MagicMock(return_value="hash") chain.download_single = MagicMock(return_value="hash")
downloads, lefts = chain.batch_download( downloads, lefts = chain.batch_download(
@@ -965,6 +983,8 @@ def test_batch_download_accepts_complete_coverage_when_files_cover_target_range(
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
chain.download_torrent = MagicMock(return_value=(b"torrent-content", "", ["demo.mkv"])) chain.download_torrent = MagicMock(return_value=(b"torrent-content", "", ["demo.mkv"]))
chain.download_single = MagicMock(return_value="hash") chain.download_single = MagicMock(return_value="hash")
@@ -997,6 +1017,8 @@ def test_batch_download_rejects_complete_coverage_when_files_have_same_count_but
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
chain.download_torrent = MagicMock(return_value=(b"torrent-content", "", ["demo.mkv"])) chain.download_torrent = MagicMock(return_value=(b"torrent-content", "", ["demo.mkv"]))
chain.download_single = MagicMock(return_value="hash") chain.download_single = MagicMock(return_value="hash")
@@ -1029,6 +1051,8 @@ def test_batch_download_accepts_complete_coverage_when_title_episodes_cover_targ
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
chain.download_torrent = MagicMock() chain.download_torrent = MagicMock()
chain.download_single = MagicMock(return_value="hash") chain.download_single = MagicMock(return_value="hash")
@@ -1062,6 +1086,8 @@ def test_batch_download_rejects_complete_coverage_when_title_episodes_are_partia
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
chain.download_torrent = MagicMock() chain.download_torrent = MagicMock()
chain.download_single = MagicMock(return_value="hash") chain.download_single = MagicMock(return_value="hash")
@@ -1095,6 +1121,8 @@ def test_batch_download_complete_coverage_ignores_allowed_episode_narrowing(monk
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
chain.download_torrent = MagicMock() chain.download_torrent = MagicMock()
chain.download_single = MagicMock(return_value="hash") chain.download_single = MagicMock(return_value="hash")
@@ -1129,6 +1157,8 @@ def test_batch_download_keeps_count_check_without_complete_coverage(monkeypatch)
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
chain = DownloadChain.__new__(DownloadChain) chain = DownloadChain.__new__(DownloadChain)
chain.eventmanager = MagicMock()
chain.eventmanager.send_event.return_value = None
chain.download_torrent = MagicMock(return_value=(b"torrent-content", "", ["demo.mkv"])) chain.download_torrent = MagicMock(return_value=(b"torrent-content", "", ["demo.mkv"]))
chain.download_single = MagicMock(return_value="hash") chain.download_single = MagicMock(return_value="hash")
+32 -24
View File
@@ -4,7 +4,7 @@
ChainBase 媒体识别插件补充MediaRecognize / MusicMediaRecognize 链式事件 ChainBase 媒体识别插件补充MediaRecognize / MusicMediaRecognize 链式事件
""" """
import asyncio import asyncio
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, MagicMock, Mock, patch
from app.chain import ChainBase from app.chain import ChainBase
from app.chain.media import MediaChain from app.chain.media import MediaChain
@@ -66,10 +66,12 @@ def test_music_recognize_help_sends_event_and_rematches(monkeypatch):
"album": "叶惠美", "album": "叶惠美",
"year": "2003", "year": "2003",
}) })
with patch("app.chain.media.eventmanager") as em: # 事件经注入的 eventmanager 发送,直接替换实例依赖并保留断言能力
em.check.return_value = True em = MagicMock()
em.send_event.return_value = event em.check.return_value = True
result = chain.recognize_by_meta(meta, media_source="musicbrainz") em.send_event.return_value = event
monkeypatch.setattr(chain, "eventmanager", em)
result = chain.recognize_by_meta(meta, media_source="musicbrainz")
assert result is remote assert result is remote
assert em.check.call_args.args[0] == ChainEventType.MusicNameRecognize assert em.check.call_args.args[0] == ChainEventType.MusicNameRecognize
@@ -101,9 +103,10 @@ def test_music_recognize_keeps_fallback_without_plugin(monkeypatch):
fallback = _fallback_music(title="未知曲目") fallback = _fallback_music(title="未知曲目")
monkeypatch.setattr(chain, "recognize_media", Mock(return_value=fallback)) monkeypatch.setattr(chain, "recognize_media", Mock(return_value=fallback))
with patch("app.chain.media.eventmanager") as em: em = MagicMock()
em.check.return_value = False em.check.return_value = False
result = chain.recognize_by_meta(meta) monkeypatch.setattr(chain, "eventmanager", em)
result = chain.recognize_by_meta(meta)
assert result is fallback assert result is fallback
em.send_event.assert_not_called() em.send_event.assert_not_called()
@@ -122,10 +125,11 @@ def test_music_recognize_help_same_elements_keeps_fallback(monkeypatch):
"name": "晴天", "name": "晴天",
"artist": "周杰伦", "artist": "周杰伦",
}) })
with patch("app.chain.media.eventmanager") as em: em = MagicMock()
em.check.return_value = True em.check.return_value = True
em.send_event.return_value = event em.send_event.return_value = event
result = chain.recognize_by_meta(meta) monkeypatch.setattr(chain, "eventmanager", em)
result = chain.recognize_by_meta(meta)
assert result is fallback assert result is fallback
assert recognize_mock.call_count == 1 assert recognize_mock.call_count == 1
@@ -143,10 +147,11 @@ def test_music_recognize_help_keeps_fallback_when_rematch_fails(monkeypatch):
"name": "另一个晴天", "name": "另一个晴天",
"artist": "未知艺术家", "artist": "未知艺术家",
}) })
with patch("app.chain.media.eventmanager") as em: em = MagicMock()
em.check.return_value = True em.check.return_value = True
em.send_event.return_value = event em.send_event.return_value = event
result = chain.recognize_by_meta(meta) monkeypatch.setattr(chain, "eventmanager", em)
result = chain.recognize_by_meta(meta)
assert result is fallback assert result is fallback
@@ -169,10 +174,12 @@ def test_async_music_recognize_help(monkeypatch):
"name": "晴天", "name": "晴天",
"artist": "周杰伦", "artist": "周杰伦",
}) })
with patch("app.chain.media.eventmanager") as em: # 事件经注入的 eventmanager 发送,直接替换实例依赖并保留断言能力
em.check.return_value = True em = MagicMock()
em.async_send_event = AsyncMock(return_value=event) em.check.return_value = True
result = asyncio.run(chain.async_recognize_by_meta(meta)) em.async_send_event = AsyncMock(return_value=event)
monkeypatch.setattr(chain, "eventmanager", em)
result = asyncio.run(chain.async_recognize_by_meta(meta))
assert result is remote assert result is remote
assert recognize_calls[-1].title == "晴天" assert recognize_calls[-1].title == "晴天"
@@ -193,10 +200,11 @@ def test_plugin_first_keeps_fallback_when_help_unidentified(monkeypatch):
"title": "晴天", "title": "晴天",
"name": "另一个晴天", "name": "另一个晴天",
}) })
with patch("app.chain.media.eventmanager") as em, \ em = MagicMock()
patch("app.runtime.config.settings.RECOGNIZE_PLUGIN_FIRST", True): em.check.return_value = True
em.check.return_value = True em.send_event.return_value = event
em.send_event.return_value = event monkeypatch.setattr(chain, "eventmanager", em)
with patch("app.runtime.config.settings.RECOGNIZE_PLUGIN_FIRST", True):
result = chain.recognize_by_meta(meta) result = chain.recognize_by_meta(meta)
assert result is fallback assert result is fallback