Merge remote-tracking branch 'origin/v2' into v2

This commit is contained in:
jxxghp
2026-05-08 15:16:34 +08:00
5 changed files with 139 additions and 23 deletions
+39 -14
View File
@@ -374,8 +374,13 @@ class SearchChain(ChainBase):
logger.warn(f'{title} 未搜索到资源') logger.warn(f'{title} 未搜索到资源')
return [] return []
# 组装上下文 # 组装上下文
contexts = [Context(meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description), contexts = [
torrent_info=torrent) for torrent in torrents] Context(
meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description),
torrent_info=torrent,
resource_source="search",
) for torrent in torrents
]
# 保存到本地文件 # 保存到本地文件
if cache_local: if cache_local:
self.save_cache(contexts, self.__result_temp_file) self.save_cache(contexts, self.__result_temp_file)
@@ -446,8 +451,13 @@ class SearchChain(ChainBase):
logger.warn(f'{title} 未搜索到资源') logger.warn(f'{title} 未搜索到资源')
return [] return []
# 组装上下文 # 组装上下文
contexts = [Context(meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description), contexts = [
torrent_info=torrent) for torrent in torrents] Context(
meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description),
torrent_info=torrent,
resource_source="search",
) for torrent in torrents
]
# 保存到本地文件 # 保存到本地文件
if cache_local: if cache_local:
await self.async_save_cache(contexts, self.__result_temp_file) 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): async for event in self.__async_search_all_sites_stream(keyword=title, sites=sites, page=page):
result = event.pop("items", []) or [] result = event.pop("items", []) or []
batch_contexts = [ batch_contexts = [
Context(meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description), Context(
torrent_info=torrent) meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description),
torrent_info=torrent,
resource_source="search",
)
for torrent in result for torrent in result
] ]
if batch_contexts: if batch_contexts:
@@ -718,7 +731,7 @@ class SearchChain(ChainBase):
and mediainfo.imdb_id \ and mediainfo.imdb_id \
and torrent.imdbid == mediainfo.imdb_id: and torrent.imdbid == mediainfo.imdb_id:
logger.info(f'{mediainfo.title} 通过IMDBID匹配到资源:{torrent.site_name} - {torrent.title}') 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 continue
# 比对种子 # 比对种子
@@ -726,7 +739,7 @@ class SearchChain(ChainBase):
torrent_meta=torrent_meta, torrent_meta=torrent_meta,
torrent=torrent): torrent=torrent):
# 匹配成功 # 匹配成功
_match_torrents.append((torrent, torrent_meta)) _match_torrents.append((torrent, torrent_meta, "title"))
continue continue
# 匹配完成 # 匹配完成
logger.info(f"匹配完成,共匹配到 {len(_match_torrents)} 个资源") logger.info(f"匹配完成,共匹配到 {len(_match_torrents)} 个资源")
@@ -736,9 +749,17 @@ class SearchChain(ChainBase):
# 去掉mediainfo中多余的数据 # 去掉mediainfo中多余的数据
mediainfo.clear() mediainfo.clear()
# 组装上下文 # 组装上下文
contexts = [Context(torrent_info=t[0], contexts = [
media_info=mediainfo, Context(
meta_info=t[1]) for t in _match_torrents] 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: finally:
torrents.clear() torrents.clear()
del torrents del torrents
@@ -989,9 +1010,13 @@ class SearchChain(ChainBase):
result = event.pop("items", []) or [] result = event.pop("items", []) or []
torrents.extend(result) torrents.extend(result)
batch_contexts = [ batch_contexts = [
Context(meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description), Context(
media_info=mediainfo, meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description),
torrent_info=torrent) media_info=mediainfo,
torrent_info=torrent,
resource_source="search",
media_info_is_target=True,
)
for torrent in result for torrent in result
] ]
candidate_contexts.extend(batch_contexts) candidate_contexts.extend(batch_contexts)
+36
View File
@@ -833,6 +833,11 @@ class SubscribeChain(ChainBase):
re_mediainfo.clear() re_mediainfo.clear()
# 更新种子缓存 # 更新种子缓存
context.media_info = re_mediainfo 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 context.media_recognize_fail_count = 0
logger.debug(f'种子 {context.torrent_info.title} 重新识别成功') logger.debug(f'种子 {context.torrent_info.title} 重新识别成功')
@@ -941,6 +946,11 @@ class SubscribeChain(ChainBase):
torrent_mediainfo.clear() torrent_mediainfo.clear()
# 更新种子缓存 # 更新种子缓存
_context.media_info = torrent_mediainfo _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 ( if not torrent_mediainfo or (
@@ -956,6 +966,9 @@ class SubscribeChain(ChainBase):
torrent_mediainfo = mediainfo torrent_mediainfo = mediainfo
# 更新种子缓存 # 更新种子缓存
_context.media_info = mediainfo _context.media_info = mediainfo
_context.match_source = "title"
_context.candidate_recognized = False
_context.media_info_is_target = True
else: else:
continue continue
@@ -971,6 +984,18 @@ class SubscribeChain(ChainBase):
continue continue
logger.info( logger.info(
f'{mediainfo.title_year} 通过媒体ID匹配到可选资源:{torrent_info.site_name} - {torrent_info.title}') 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: else:
continue continue
@@ -2537,6 +2562,17 @@ class SubscribeChain(ChainBase):
return min_ep <= start_ep and max_ep >= end_ep 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 @staticmethod
def get_states_for_search(state: str) -> str: def get_states_for_search(state: str) -> str:
""" """
+43 -8
View File
@@ -60,8 +60,8 @@ class TorrentsChain(ChainBase):
else: else:
torrents_cache = self.load_cache(self._rss_file) or {} torrents_cache = self.load_cache(self._rss_file) or {}
# 兼容性处理:为旧版本的Context对象添加失败次数字段 # 兼容性处理:为旧版本的Context对象补齐新增候选识别字段
self._ensure_context_compatibility(torrents_cache) self._ensure_context_compatibility(torrents_cache, stype=stype)
return torrents_cache return torrents_cache
@@ -80,8 +80,8 @@ class TorrentsChain(ChainBase):
else: else:
torrents_cache = await self.async_load_cache(self._rss_file) or {} torrents_cache = await self.async_load_cache(self._rss_file) or {}
# 兼容性处理:为旧版本的Context对象添加失败次数字段 # 兼容性处理:为旧版本的Context对象补齐新增候选识别字段
self._ensure_context_compatibility(torrents_cache) self._ensure_context_compatibility(torrents_cache, stype=stype)
return torrents_cache return torrents_cache
@@ -285,8 +285,18 @@ class TorrentsChain(ChainBase):
mediainfo = MediaInfo() mediainfo = MediaInfo()
# 清理多余数据,减少内存占用 # 清理多余数据,减少内存占用
mediainfo.clear() 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 # 如果未识别到媒体信息,设置初始失败次数为1
if not mediainfo or (not mediainfo.tmdb_id and not mediainfo.douban_id): if not mediainfo or (not mediainfo.tmdb_id and not mediainfo.douban_id):
context.media_recognize_fail_count = 1 context.media_recognize_fail_count = 1
@@ -317,19 +327,44 @@ class TorrentsChain(ChainBase):
return torrents_cache return torrents_cache
@staticmethod @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对象的兼容性,为旧版本添加缺失的字段 确保Context对象的兼容性,为旧版本添加缺失的字段
""" """
for domain, contexts in torrents_cache.items(): for domain, contexts in torrents_cache.items():
for context in contexts: for context in contexts:
# 如果Context对象没有media_recognize_fail_count字段,添加默认值 context_fields = vars(context)
if not hasattr(context, 'media_recognize_fail_count'): # 旧 pickle 实例会读到 dataclass 类默认值,必须检查实例字段,避免跳过兼容回填。
if "media_recognize_fail_count" not in context_fields:
context.media_recognize_fail_count = 0 context.media_recognize_fail_count = 0
# 如果媒体信息未识别,设置初始失败次数 # 如果媒体信息未识别,设置初始失败次数
if (not context.media_info or if (not context.media_info or
(not context.media_info.tmdb_id and not context.media_info.douban_id)): (not context.media_info.tmdb_id and not context.media_info.douban_id)):
context.media_recognize_fail_count = 1 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): def __renew_rss_url(self, domain: str, site: dict):
""" """
+13 -1
View File
@@ -832,6 +832,14 @@ class Context:
torrent_info: TorrentInfo = None torrent_info: TorrentInfo = None
# 媒体识别失败次数 # 媒体识别失败次数
media_recognize_fail_count: int = 0 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): def to_dict(self):
""" """
@@ -841,5 +849,9 @@ class Context:
"meta_info": self.meta_info.to_dict() if self.meta_info else None, "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, "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_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,
} }
+8
View File
@@ -248,6 +248,14 @@ class Context(BaseModel):
media_info: Optional[Union[MediaInfo, Any]] = None media_info: Optional[Union[MediaInfo, Any]] = None
# 种子信息 # 种子信息
torrent_info: Optional[TorrentInfo] = 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): class MediaSeason(BaseModel):