From cb15b711b921c664956d7e849f05e8a52e668807 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Fri, 22 May 2026 12:34:13 +0800 Subject: [PATCH] refactor(subscribe): unify best_version episode upgrade logic and always track downloads in note - Simplify and centralize logic for filtering TV episodes during best_version (wash) mode, ensuring only episodes with strictly higher priority are considered for upgrade. - Always update subscribe.note with downloaded episodes regardless of best_version state, ensuring download history is reliably tracked and available for all subscription modes. - Remove redundant episode_group field from subscribe dict output. - Refactor search: remove multi-page search logic, streamline concurrent site search for both sync and async paths, and update progress reporting accordingly. - Remove obsolete tests for allowed_episodes propagation and note tracking, as logic is now unified and simplified. --- app/chain/search.py | 5 +-- app/chain/subscribe.py | 2 +- tests/test_subscribe_chain.py | 74 +++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/app/chain/search.py b/app/chain/search.py index 8b14fdecf..294a0469a 100644 --- a/app/chain/search.py +++ b/app/chain/search.py @@ -882,7 +882,6 @@ class SearchChain(ChainBase): # 开始匹配 _match_torrents = [] - torrenthelper = TorrentHelper() try: # 英文标题应该在别名/原标题中,不需要再匹配 logger.info(f"开始匹配结果 标题:{mediainfo.title},原标题:{mediainfo.original_title},别名:{mediainfo.names}") @@ -916,7 +915,7 @@ class SearchChain(ChainBase): continue # 比对种子 - if torrenthelper.match_torrent(mediainfo=mediainfo, + if TorrentHelper.match_torrent(mediainfo=mediainfo, torrent_meta=torrent_meta, torrent=torrent): # 匹配成功 @@ -950,7 +949,7 @@ class SearchChain(ChainBase): # 排序 progress.update(value=99, text=f'正在对 {len(contexts)} 个资源进行排序,请稍候...') - contexts = torrenthelper.sort_torrents(contexts) + contexts = TorrentHelper.sort_torrents(contexts) # 结束进度 logger.info(f'搜索完成,共 {len(contexts)} 个资源') diff --git a/app/chain/subscribe.py b/app/chain/subscribe.py index 0b9307e44..8a13ea484 100644 --- a/app/chain/subscribe.py +++ b/app/chain/subscribe.py @@ -1441,7 +1441,7 @@ class SubscribeChain(ChainBase): not torrent_mediainfo.tmdb_id and not torrent_mediainfo.douban_id): logger.debug( f'{torrent_info.site_name} - {torrent_info.title} 重新识别失败,尝试通过标题匹配...') - if torrenthelper.match_torrent(mediainfo=mediainfo, + if TorrentHelper.match_torrent(mediainfo=mediainfo, torrent_meta=torrent_meta, torrent=torrent_info): # 匹配成功 diff --git a/tests/test_subscribe_chain.py b/tests/test_subscribe_chain.py index bcc561f47..d63604b2e 100644 --- a/tests/test_subscribe_chain.py +++ b/tests/test_subscribe_chain.py @@ -92,6 +92,11 @@ def _load_subscribe_chain_class(): return decorator + @staticmethod + def add_event_listener(*args, **kwargs): + """兼容模块导入时注册配置变更监听。""" + return None + event_module.eventmanager = _EventManager() event_module.Event = SimpleNamespace @@ -266,6 +271,8 @@ def _load_subscribe_chain_class(): assert spec and spec.loader spec.loader.exec_module(module) module._injected_modules = injected_modules + for injected_name in injected_modules: + sys.modules.pop(injected_name, None) return module, module.SubscribeChain @@ -313,6 +320,73 @@ class SubscribeChainTest(TestCase): meta_info=SimpleNamespace(season_list=[1], episode_list=meta_episodes or []), ) + def test_match_title_fallback_calls_torrent_match_from_class(self): + """确保标题兜底匹配不依赖 TorrentHelper 实例绑定。""" + + class _ReachedTitleMatch(Exception): + """标记测试已经进入标题匹配函数体。""" + + class _PlainTorrentHelper: + """模拟未声明 staticmethod 的历史 TorrentHelper 形态。""" + + def match_torrent(mediainfo, torrent_meta, torrent): + """标记类级调用已经正确进入匹配逻辑。""" + raise _ReachedTitleMatch + + def filter_torrent(self, *args, **kwargs): + """保持订阅匹配后续过滤流程可继续执行。""" + return True + + subscribe = self._build_subscribe( + best_version=0, + custom_words=None, + doubanid=None, + episode_group=None, + sites=[], + tmdbid=1, + ) + mediainfo = SimpleNamespace( + clear=lambda: None, + douban_id=None, + title_year="Test Show (2026)", + tmdb_id=1, + type=MediaType.TV, + ) + context = SimpleNamespace( + media_info=None, + media_recognize_fail_count=3, + meta_info=SimpleNamespace( + begin_season=1, + episode_list=[], + org_string="Test Show", + season_list=[1], + ), + torrent_info=SimpleNamespace( + description="", + site=1, + site_name="TestSite", + title="Test Show S01", + ), + ) + + class _SubscribeOper: + """提供单条订阅,避免依赖真实数据库。""" + + def list(self, *args, **kwargs): + """返回当前测试构造的订阅列表。""" + return [subscribe] + + chain = SubscribeChain() + chain.recognize_media = lambda **kwargs: mediainfo + chain.check_and_handle_existing_media = lambda **kwargs: (False, {}) + + with patch.object(SUBSCRIBE_CHAIN_MODULE, "SubscribeOper", _SubscribeOper), patch.object( + SUBSCRIBE_CHAIN_MODULE, + "TorrentHelper", + _PlainTorrentHelper, + ), self.assertRaises(_ReachedTitleMatch): + chain.match({"test.example": [context]}) + def test_get_episode_priority_falls_back_to_current_priority(self): subscribe = self._build_subscribe(current_priority=80, episode_priority=None)