mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 08:26:53 +08:00
feat(subscribe): expose cached site candidates (#6062)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import copy
|
||||
import re
|
||||
import traceback
|
||||
from typing import Callable, Dict, List, Union, Optional
|
||||
@@ -92,6 +93,238 @@ class TorrentsChain(ChainBase):
|
||||
|
||||
return torrents_cache
|
||||
|
||||
def get_subscribe_cache_candidates(
|
||||
self,
|
||||
subscribe,
|
||||
stype: Optional[str] = None,
|
||||
allow_title_match: bool = False,
|
||||
) -> List[Context]:
|
||||
"""
|
||||
按订阅身份读取 RSS/spider 缓存候选,返回不会回写缓存的 Context 副本。
|
||||
|
||||
主程序只提供缓存读取与轻量候选筛选,不在这里判断站点证据能否扩展
|
||||
订阅目标或放行完成;标题兜底候选会显式标记为低置信来源。
|
||||
"""
|
||||
results: List[Context] = []
|
||||
for contexts in (self.get_torrents(stype=stype) or {}).values():
|
||||
for context in contexts or []:
|
||||
if not context:
|
||||
continue
|
||||
copied = copy.deepcopy(context)
|
||||
if self._context_matches_subscribe(copied, subscribe):
|
||||
results.append(copied)
|
||||
continue
|
||||
if allow_title_match and self._context_title_matches_subscribe(copied, subscribe):
|
||||
self._mark_title_match_candidate(copied, subscribe)
|
||||
results.append(copied)
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def _context_matches_subscribe(cls, context: Context, subscribe) -> bool:
|
||||
"""
|
||||
严格身份匹配:候选自身识别出的媒体 ID 命中订阅,且季信息不排除订阅季。
|
||||
"""
|
||||
if not cls._context_media_type_matches(context, subscribe):
|
||||
return False
|
||||
if not cls._context_season_matches_subscribe(context, subscribe):
|
||||
return False
|
||||
|
||||
subscribe_tmdbid = cls._normalize_id(getattr(subscribe, "tmdbid", None))
|
||||
subscribe_doubanid = cls._normalize_id(getattr(subscribe, "doubanid", None))
|
||||
context_tmdbids = cls._context_tmdb_ids(context)
|
||||
context_doubanids = cls._context_douban_ids(context)
|
||||
|
||||
return bool(
|
||||
subscribe_tmdbid and subscribe_tmdbid in context_tmdbids
|
||||
or subscribe_doubanid and subscribe_doubanid in context_doubanids
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _context_title_matches_subscribe(cls, context: Context, subscribe) -> bool:
|
||||
"""
|
||||
标题兜底只服务诊断:仅允许身份缺失候选按标题命中,显式冲突 ID 不兜底。
|
||||
"""
|
||||
if cls._context_has_media_identity(context):
|
||||
return False
|
||||
if not cls._context_media_type_matches(context, subscribe):
|
||||
return False
|
||||
if not cls._context_season_matches_subscribe(context, subscribe):
|
||||
return False
|
||||
|
||||
subscribe_title = cls._normalize_title(getattr(subscribe, "name", None))
|
||||
if not subscribe_title:
|
||||
return False
|
||||
|
||||
meta_info = getattr(context, "meta_info", None)
|
||||
torrent_info = getattr(context, "torrent_info", None)
|
||||
candidate_titles = [
|
||||
getattr(torrent_info, "title", None),
|
||||
getattr(meta_info, "title", None),
|
||||
getattr(meta_info, "name", None),
|
||||
]
|
||||
return any(
|
||||
subscribe_title in candidate_title
|
||||
for candidate_title in (cls._normalize_title(title) for title in candidate_titles)
|
||||
if candidate_title
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _mark_title_match_candidate(context: Context, subscribe) -> None:
|
||||
"""
|
||||
标记标题兜底候选,避免下游把目标媒体回填误认为候选自身识别结果。
|
||||
"""
|
||||
context.match_source = "title"
|
||||
context.candidate_recognized = False
|
||||
context.media_info_is_target = True
|
||||
context.media_info = MediaInfo(
|
||||
type=getattr(subscribe, "type", None),
|
||||
title=getattr(subscribe, "name", None),
|
||||
tmdb_id=getattr(subscribe, "tmdbid", None),
|
||||
douban_id=getattr(subscribe, "doubanid", None),
|
||||
season=getattr(subscribe, "season", None),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _context_media_type_matches(cls, context: Context, subscribe) -> bool:
|
||||
"""
|
||||
类型已知且冲突时拒绝;缺失类型不作为缓存候选过滤条件。
|
||||
"""
|
||||
subscribe_type = cls._normalize_media_type(getattr(subscribe, "type", None))
|
||||
media_info = getattr(context, "media_info", None)
|
||||
meta_info = getattr(context, "meta_info", None)
|
||||
context_types = {
|
||||
cls._normalize_media_type(value)
|
||||
for value in (
|
||||
getattr(media_info, "type", None),
|
||||
getattr(meta_info, "type", None),
|
||||
)
|
||||
}
|
||||
context_types.discard(None)
|
||||
return not subscribe_type or not context_types or all(
|
||||
context_type == subscribe_type for context_type in context_types
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _context_season_matches_subscribe(cls, context: Context, subscribe) -> bool:
|
||||
"""
|
||||
资源季信息只要明确排除订阅季就拒绝;跨季覆盖目标季留给插件诊断。
|
||||
"""
|
||||
target_season = cls._normalize_int(getattr(subscribe, "season", None))
|
||||
if target_season is None:
|
||||
return True
|
||||
|
||||
meta_info = getattr(context, "meta_info", None)
|
||||
explicit_meta_seasons = cls._context_meta_seasons(meta_info)
|
||||
if explicit_meta_seasons:
|
||||
return target_season in explicit_meta_seasons
|
||||
|
||||
media_info = getattr(context, "media_info", None)
|
||||
media_season = cls._normalize_int(getattr(media_info, "season", None))
|
||||
return media_season is None or target_season == media_season
|
||||
|
||||
@classmethod
|
||||
def _context_meta_seasons(cls, meta_info) -> set[int]:
|
||||
"""
|
||||
提取标题解析出的显式季范围;多季包以该范围为准。
|
||||
"""
|
||||
meta_fields = vars(meta_info) if meta_info else {}
|
||||
if "season_list" in meta_fields:
|
||||
season_list = {
|
||||
season
|
||||
for season in (
|
||||
cls._normalize_int(item)
|
||||
for item in (meta_fields.get("season_list") or [])
|
||||
)
|
||||
if season is not None
|
||||
}
|
||||
if season_list:
|
||||
return season_list
|
||||
begin_season = cls._normalize_int(getattr(meta_info, "begin_season", None))
|
||||
end_season = cls._normalize_int(getattr(meta_info, "end_season", None))
|
||||
if begin_season is not None and end_season is not None:
|
||||
start, end = sorted((begin_season, end_season))
|
||||
return set(range(start, end + 1))
|
||||
if begin_season is not None:
|
||||
return {begin_season}
|
||||
if end_season is not None:
|
||||
return {end_season}
|
||||
return set()
|
||||
|
||||
@staticmethod
|
||||
def _context_has_media_identity(context: Context) -> bool:
|
||||
"""
|
||||
判断候选是否已经带有明确媒体 ID。
|
||||
"""
|
||||
return bool(TorrentsChain._context_tmdb_ids(context) or TorrentsChain._context_douban_ids(context))
|
||||
|
||||
@staticmethod
|
||||
def _context_tmdb_ids(context: Context) -> set[str]:
|
||||
"""
|
||||
提取候选已有 TMDB ID,兼容 media_info 与标题显式标签。
|
||||
"""
|
||||
media_info = getattr(context, "media_info", None)
|
||||
meta_info = getattr(context, "meta_info", None)
|
||||
return {
|
||||
value for value in (
|
||||
TorrentsChain._normalize_id(getattr(media_info, "tmdb_id", None)),
|
||||
TorrentsChain._normalize_id(getattr(meta_info, "tmdbid", None)),
|
||||
) if value
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _context_douban_ids(context: Context) -> set[str]:
|
||||
"""
|
||||
提取候选已有豆瓣 ID,兼容 media_info 与标题显式标签。
|
||||
"""
|
||||
media_info = getattr(context, "media_info", None)
|
||||
meta_info = getattr(context, "meta_info", None)
|
||||
return {
|
||||
value for value in (
|
||||
TorrentsChain._normalize_id(getattr(media_info, "douban_id", None)),
|
||||
TorrentsChain._normalize_id(getattr(meta_info, "doubanid", None)),
|
||||
) if value
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_id(value) -> Optional[str]:
|
||||
"""
|
||||
统一比较媒体 ID,避免 int/string 形态差异影响缓存候选筛选。
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
value = str(value).strip()
|
||||
return value or None
|
||||
|
||||
@staticmethod
|
||||
def _normalize_int(value) -> Optional[int]:
|
||||
"""
|
||||
将季号等动态字段转为 int,无法解析时视为缺失。
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _normalize_media_type(value) -> Optional[str]:
|
||||
"""
|
||||
统一 MediaType 枚举与字符串形态。
|
||||
"""
|
||||
if isinstance(value, MediaType):
|
||||
value = value.value
|
||||
if value == MediaType.UNKNOWN.value:
|
||||
return None
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _normalize_title(value) -> str:
|
||||
"""
|
||||
归一标题用于低置信标题兜底匹配。
|
||||
"""
|
||||
return (StringUtils.clear_upper(value or "") or "").strip()
|
||||
|
||||
def clear_torrents(self):
|
||||
"""
|
||||
清理种子缓存数据
|
||||
|
||||
Reference in New Issue
Block a user