diff --git a/app/chain/search.py b/app/chain/search.py index ac0aefd7..b880fdef 100644 --- a/app/chain/search.py +++ b/app/chain/search.py @@ -374,8 +374,13 @@ class SearchChain(ChainBase): logger.warn(f'{title} 未搜索到资源') return [] # 组装上下文 - contexts = [Context(meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description), - torrent_info=torrent) for torrent in torrents] + contexts = [ + Context( + meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description), + torrent_info=torrent, + resource_source="search", + ) for torrent in torrents + ] # 保存到本地文件 if cache_local: self.save_cache(contexts, self.__result_temp_file) @@ -446,8 +451,13 @@ class SearchChain(ChainBase): logger.warn(f'{title} 未搜索到资源') return [] # 组装上下文 - contexts = [Context(meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description), - torrent_info=torrent) for torrent in torrents] + contexts = [ + Context( + meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description), + torrent_info=torrent, + resource_source="search", + ) for torrent in torrents + ] # 保存到本地文件 if cache_local: await self.async_save_cache(contexts, self.__result_temp_file) @@ -470,8 +480,11 @@ class SearchChain(ChainBase): async for event in self.__async_search_all_sites_stream(keyword=title, sites=sites, page=page): result = event.pop("items", []) or [] batch_contexts = [ - Context(meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description), - torrent_info=torrent) + Context( + meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description), + torrent_info=torrent, + resource_source="search", + ) for torrent in result ] if batch_contexts: @@ -718,7 +731,7 @@ class SearchChain(ChainBase): and mediainfo.imdb_id \ and torrent.imdbid == mediainfo.imdb_id: logger.info(f'{mediainfo.title} 通过IMDBID匹配到资源:{torrent.site_name} - {torrent.title}') - _match_torrents.append((torrent, torrent_meta)) + _match_torrents.append((torrent, torrent_meta, "imdbid")) continue # 比对种子 @@ -726,7 +739,7 @@ class SearchChain(ChainBase): torrent_meta=torrent_meta, torrent=torrent): # 匹配成功 - _match_torrents.append((torrent, torrent_meta)) + _match_torrents.append((torrent, torrent_meta, "title")) continue # 匹配完成 logger.info(f"匹配完成,共匹配到 {len(_match_torrents)} 个资源") @@ -736,9 +749,17 @@ class SearchChain(ChainBase): # 去掉mediainfo中多余的数据 mediainfo.clear() # 组装上下文 - contexts = [Context(torrent_info=t[0], - media_info=mediainfo, - meta_info=t[1]) for t in _match_torrents] + contexts = [ + Context( + torrent_info=t[0], + media_info=mediainfo, + meta_info=t[1], + resource_source="search", + match_source=t[2], + candidate_recognized=False, + media_info_is_target=True, + ) for t in _match_torrents + ] finally: torrents.clear() del torrents @@ -989,9 +1010,13 @@ class SearchChain(ChainBase): result = event.pop("items", []) or [] torrents.extend(result) batch_contexts = [ - Context(meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description), - media_info=mediainfo, - torrent_info=torrent) + Context( + meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description), + media_info=mediainfo, + torrent_info=torrent, + resource_source="search", + media_info_is_target=True, + ) for torrent in result ] candidate_contexts.extend(batch_contexts) diff --git a/app/chain/subscribe.py b/app/chain/subscribe.py index 814fffb3..99539d5b 100644 --- a/app/chain/subscribe.py +++ b/app/chain/subscribe.py @@ -833,6 +833,11 @@ class SubscribeChain(ChainBase): re_mediainfo.clear() # 更新种子缓存 context.media_info = re_mediainfo + context.match_source = self.__get_media_id_match_source(re_mediainfo) + context.candidate_recognized = bool( + re_mediainfo.tmdb_id or re_mediainfo.douban_id + ) + context.media_info_is_target = False # 重置失败次数 context.media_recognize_fail_count = 0 logger.debug(f'种子 {context.torrent_info.title} 重新识别成功') @@ -941,6 +946,11 @@ class SubscribeChain(ChainBase): torrent_mediainfo.clear() # 更新种子缓存 _context.media_info = torrent_mediainfo + _context.match_source = self.__get_media_id_match_source(torrent_mediainfo) + _context.candidate_recognized = bool( + torrent_mediainfo.tmdb_id or torrent_mediainfo.douban_id + ) + _context.media_info_is_target = False # 如果仍然没有识别到媒体信息,尝试标题匹配 if not torrent_mediainfo or ( @@ -956,6 +966,9 @@ class SubscribeChain(ChainBase): torrent_mediainfo = mediainfo # 更新种子缓存 _context.media_info = mediainfo + _context.match_source = "title" + _context.candidate_recognized = False + _context.media_info_is_target = True else: continue @@ -971,6 +984,18 @@ class SubscribeChain(ChainBase): continue logger.info( f'{mediainfo.title_year} 通过媒体ID匹配到可选资源:{torrent_info.site_name} - {torrent_info.title}') + match_source = getattr(_context, "match_source", "unknown") + if match_source == "title": + # 标题兜底使用的是订阅目标 media_info,不能标记为候选自身识别结果。 + _context.candidate_recognized = False + _context.media_info_is_target = True + elif match_source == "unknown": + _context.match_source = self.__get_media_id_match_source(torrent_mediainfo) + _context.candidate_recognized = True + _context.media_info_is_target = False + else: + _context.candidate_recognized = True + _context.media_info_is_target = False else: continue @@ -2537,6 +2562,17 @@ class SubscribeChain(ChainBase): return min_ep <= start_ep and max_ep >= end_ep + @staticmethod + def __get_media_id_match_source(mediainfo: Optional[MediaInfo]) -> str: + """ + 返回候选自身识别命中的明确媒体 ID 类型。 + """ + if mediainfo and mediainfo.tmdb_id: + return "tmdbid" + if mediainfo and mediainfo.douban_id: + return "doubanid" + return "unknown" + @staticmethod def get_states_for_search(state: str) -> str: """ diff --git a/app/chain/torrents.py b/app/chain/torrents.py index 8ee07a80..956d8a9e 100644 --- a/app/chain/torrents.py +++ b/app/chain/torrents.py @@ -60,8 +60,8 @@ class TorrentsChain(ChainBase): else: torrents_cache = self.load_cache(self._rss_file) or {} - # 兼容性处理:为旧版本的Context对象添加失败次数字段 - self._ensure_context_compatibility(torrents_cache) + # 兼容性处理:为旧版本的Context对象补齐新增候选识别字段 + self._ensure_context_compatibility(torrents_cache, stype=stype) return torrents_cache @@ -80,8 +80,8 @@ class TorrentsChain(ChainBase): else: torrents_cache = await self.async_load_cache(self._rss_file) or {} - # 兼容性处理:为旧版本的Context对象添加失败次数字段 - self._ensure_context_compatibility(torrents_cache) + # 兼容性处理:为旧版本的Context对象补齐新增候选识别字段 + self._ensure_context_compatibility(torrents_cache, stype=stype) return torrents_cache @@ -285,8 +285,18 @@ class TorrentsChain(ChainBase): mediainfo = MediaInfo() # 清理多余数据,减少内存占用 mediainfo.clear() + candidate_recognized = bool(mediainfo and (mediainfo.tmdb_id or mediainfo.douban_id)) + match_source = self._get_media_id_match_source(mediainfo) # 上下文 - context = Context(meta_info=meta, media_info=mediainfo, torrent_info=torrent) + context = Context( + meta_info=meta, + media_info=mediainfo, + torrent_info=torrent, + resource_source="spider" if stype == "spider" else "rss", + match_source=match_source if candidate_recognized else "unknown", + candidate_recognized=candidate_recognized, + media_info_is_target=False, + ) # 如果未识别到媒体信息,设置初始失败次数为1 if not mediainfo or (not mediainfo.tmdb_id and not mediainfo.douban_id): context.media_recognize_fail_count = 1 @@ -317,19 +327,44 @@ class TorrentsChain(ChainBase): return torrents_cache @staticmethod - def _ensure_context_compatibility(torrents_cache: Dict[str, List[Context]]): + def _ensure_context_compatibility(torrents_cache: Dict[str, List[Context]], stype: Optional[str] = None): """ 确保Context对象的兼容性,为旧版本添加缺失的字段 """ for domain, contexts in torrents_cache.items(): for context in contexts: - # 如果Context对象没有media_recognize_fail_count字段,添加默认值 - if not hasattr(context, 'media_recognize_fail_count'): + context_fields = vars(context) + # 旧 pickle 实例会读到 dataclass 类默认值,必须检查实例字段,避免跳过兼容回填。 + if "media_recognize_fail_count" not in context_fields: context.media_recognize_fail_count = 0 # 如果媒体信息未识别,设置初始失败次数 if (not context.media_info or (not context.media_info.tmdb_id and not context.media_info.douban_id)): context.media_recognize_fail_count = 1 + if "resource_source" not in context_fields: + context.resource_source = "spider" if stype == "spider" else "rss" + if "candidate_recognized" not in context_fields: + context.candidate_recognized = bool( + context.media_info and (context.media_info.tmdb_id or context.media_info.douban_id) + ) + if "match_source" not in context_fields: + context.match_source = ( + TorrentsChain._get_media_id_match_source(context.media_info) + if context.candidate_recognized else "unknown" + ) + if "media_info_is_target" not in context_fields: + context.media_info_is_target = False + + @staticmethod + def _get_media_id_match_source(mediainfo: Optional[MediaInfo]) -> str: + """ + 返回候选自身识别命中的明确媒体 ID 类型。 + """ + if mediainfo and mediainfo.tmdb_id: + return "tmdbid" + if mediainfo and mediainfo.douban_id: + return "doubanid" + return "unknown" def __renew_rss_url(self, domain: str, site: dict): """ diff --git a/app/core/context.py b/app/core/context.py index 9a8e840a..f77b2349 100644 --- a/app/core/context.py +++ b/app/core/context.py @@ -832,6 +832,14 @@ class Context: torrent_info: TorrentInfo = None # 媒体识别失败次数 media_recognize_fail_count: int = 0 + # 候选资源来源:rss、spider、search、unknown。 + resource_source: str = "unknown" + # 候选匹配来源:tmdbid、doubanid、imdbid、title、plugin、unknown。 + match_source: str = "unknown" + # 候选自身是否已经识别出有效媒体 ID。 + candidate_recognized: bool = False + # 当前 media_info 是否为目标媒体回填,而不是候选自身识别结果。 + media_info_is_target: bool = False def to_dict(self): """ @@ -841,5 +849,9 @@ class Context: "meta_info": self.meta_info.to_dict() if self.meta_info else None, "torrent_info": self.torrent_info.to_dict() if self.torrent_info else None, "media_info": self.media_info.to_dict() if self.media_info else None, - "media_recognize_fail_count": self.media_recognize_fail_count + "media_recognize_fail_count": self.media_recognize_fail_count, + "resource_source": self.resource_source, + "match_source": self.match_source, + "candidate_recognized": self.candidate_recognized, + "media_info_is_target": self.media_info_is_target, } diff --git a/app/schemas/context.py b/app/schemas/context.py index cf8516a1..5fe04c14 100644 --- a/app/schemas/context.py +++ b/app/schemas/context.py @@ -248,6 +248,14 @@ class Context(BaseModel): media_info: Optional[Union[MediaInfo, Any]] = None # 种子信息 torrent_info: Optional[TorrentInfo] = None + # 候选资源来源:rss、spider、search、unknown + resource_source: Optional[str] = "unknown" + # 候选匹配来源:tmdbid、doubanid、imdbid、title、plugin、unknown + match_source: Optional[str] = "unknown" + # 候选自身是否已经识别出有效媒体 ID + candidate_recognized: Optional[bool] = False + # 当前 media_info 是否为目标媒体回填 + media_info_is_target: Optional[bool] = False class MediaSeason(BaseModel):