feat(subscribe): expose missing target resolver (#5958)

* feat(subscribe): expose missing target resolver

* fix(subscribe): refine missing resolver semantics
This commit is contained in:
InfinityPacer
2026-06-17 19:54:02 +08:00
committed by GitHub
parent 846eed6821
commit 4ecbc677eb
2 changed files with 538 additions and 84 deletions

View File

@@ -76,16 +76,17 @@ class SubscribeChain(ChainBase):
return normalized
@classmethod
def __get_episode_priority(cls, subscribe: Subscribe) -> Dict[str, int]:
def __get_episode_priority(cls, subscribe: Subscribe,
total_episode: Optional[int] = None) -> Dict[str, int]:
"""
获取订阅按集洗版优先级状态。
"""
episode_priority = cls.__normalize_episode_priority(getattr(subscribe, "episode_priority", None))
episode_priority = cls.__normalize_episode_priority(subscribe.episode_priority)
if episode_priority:
return episode_priority
if subscribe.best_version and subscribe.type == MediaType.TV.value and subscribe.current_priority is not None:
target_episodes = cls.__get_best_version_target_episodes(subscribe)
target_episodes = cls.__get_best_version_target_episodes(subscribe, total_episode=total_episode)
return {
str(episode): int(subscribe.current_priority)
for episode in target_episodes
@@ -100,7 +101,8 @@ class SubscribeChain(ChainBase):
return cls.__get_episode_priority(subscribe)
@classmethod
def __get_best_version_target_episodes(cls, subscribe: Subscribe) -> List[int]:
def __get_best_version_target_episodes(cls, subscribe: Subscribe,
total_episode: Optional[int] = None) -> List[int]:
"""
获取洗版订阅目标剧集范围。
"""
@@ -108,36 +110,75 @@ class SubscribeChain(ChainBase):
return []
start_episode = subscribe.start_episode or 1
total_episode = subscribe.total_episode or 0
total_episode = total_episode or subscribe.total_episode or 0
if total_episode < start_episode:
return []
return list(range(start_episode, total_episode + 1))
@classmethod
def __get_downloaded_best_version_episodes(cls, subscribe: Subscribe,
total_episode: Optional[int] = None) -> List[int]:
"""
获取洗版订阅目标范围内已下载到任意版本的剧集。
分集洗版的完成态要求 priority==100但订阅目标满足查询有时只需要确认
目标集是否已下载过任意版本,因此这里按 note 与 episode_priority>0 统计。
"""
if subscribe.type != MediaType.TV.value:
return []
start_episode = subscribe.start_episode or 1
total_episode = total_episode or subscribe.total_episode or 0
if total_episode < start_episode:
return []
target_episodes = set(range(start_episode, total_episode + 1))
downloaded = set()
for episode in subscribe.note or []:
try:
episode_number = int(episode)
except (TypeError, ValueError):
continue
if episode_number in target_episodes:
downloaded.add(episode_number)
for episode, priority in (subscribe.episode_priority or {}).items():
if not str(episode).isdigit():
continue
try:
if float(priority) > 0:
episode_number = int(episode)
if episode_number in target_episodes:
downloaded.add(episode_number)
except (TypeError, ValueError):
continue
return sorted(downloaded)
@classmethod
def __get_pending_best_version_episodes_with_priority(
cls,
subscribe: Subscribe,
episode_priority: Optional[dict] = None,
total_episode: Optional[int] = None,
) -> List[int]:
"""
使用指定按集优先级状态获取当前仍需继续洗版的剧集。
"""
target_episodes = cls.__get_best_version_target_episodes(subscribe)
target_episodes = cls.__get_best_version_target_episodes(subscribe, total_episode=total_episode)
if not target_episodes:
return []
if episode_priority is None:
normalized = cls.__get_episode_priority(subscribe)
normalized = cls.__get_episode_priority(subscribe, total_episode=total_episode)
else:
normalized = cls.__normalize_episode_priority(episode_priority)
return [episode for episode in target_episodes if normalized.get(str(episode)) != 100]
@classmethod
def _get_pending_best_version_episodes(cls, subscribe: Subscribe) -> List[int]:
def _get_pending_best_version_episodes(cls, subscribe: Subscribe,
total_episode: Optional[int] = None) -> List[int]:
"""
获取当前仍需继续洗版的剧集。
"""
return cls.__get_pending_best_version_episodes_with_priority(subscribe)
return cls.__get_pending_best_version_episodes_with_priority(subscribe, total_episode=total_episode)
@classmethod
def compute_completed_episode(cls, subscribe: Subscribe) -> Optional[int]:
@@ -324,7 +365,7 @@ class SubscribeChain(ChainBase):
判断当前订阅是否启用了电视剧全集洗版。
"""
return (
bool(getattr(subscribe, "best_version_full", 0))
bool(subscribe.best_version_full)
and bool(subscribe.best_version)
and subscribe.type == MediaType.TV.value
)
@@ -2813,7 +2854,8 @@ class SubscribeChain(ChainBase):
season=begin_season,
episodes=episodes,
total_episode=total_episode,
start_episode=start_episode
start_episode=start_episode,
require_complete_coverage=no_exist_season.require_complete_coverage
)
# 根据订阅已下载集数更新缺失集数
if downloaded_episodes:
@@ -2841,6 +2883,7 @@ class SubscribeChain(ChainBase):
episodes=episodes,
total_episode=total,
start_episode=start,
require_complete_coverage=no_exist_season.require_complete_coverage
)
else:
# 开始集数
@@ -2855,6 +2898,7 @@ class SubscribeChain(ChainBase):
episodes=episodes,
total_episode=total_episode,
start_episode=start,
require_complete_coverage=False,
)
logger.info(f'订阅 {subscribe_name} 缺失剧集数更新为:{no_exists}')
return False, no_exists
@@ -3070,72 +3114,12 @@ class SubscribeChain(ChainBase):
"""
self.__refresh_total_episode_before_completion(subscribe=subscribe, mediainfo=mediainfo)
# 非洗版
if not subscribe.best_version:
# 每季总集数
totals = {}
if subscribe.season and subscribe.total_episode:
totals = {
subscribe.season: subscribe.total_episode
}
# 查询媒体库缺失的媒体信息
exist_flag, no_exists = DownloadChain().get_no_exists_info(
meta=meta,
mediainfo=mediainfo,
totals=totals
)
else:
# 洗版,如果已经满足了优先级,则认为已经洗版完成
if self.__is_best_version_complete(subscribe):
exist_flag = True
no_exists = {}
else:
exist_flag = False
if meta.type == MediaType.TV:
pending_episodes = [] if self.__is_full_best_version_enabled(
subscribe
) else self._get_pending_best_version_episodes(subscribe)
# 对于电视剧,构造缺失的媒体信息
no_exists = {
mediakey: {
subscribe.season: schemas.NotExistMediaInfo(
season=subscribe.season,
episodes=pending_episodes,
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode or 1,
# 完整覆盖约束会影响整季文件探测、显式集列表匹配和多集拆包降级。
require_complete_coverage=self.__is_full_best_version_enabled(subscribe))
}
}
else:
no_exists = {}
# 如果媒体已存在,执行订阅完成操作
if exist_flag:
if not subscribe.best_version:
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, force=True)
return True, no_exists
# 获取已下载的集数或电影
downloaded = self.__get_downloaded(subscribe)
if self.__is_full_best_version_enabled(subscribe):
# 全集洗版必须保留整季缺失范围,避免下载链路从整包中拆选单集。
downloaded = []
if meta.type == MediaType.TV:
# 对于电视剧类型,整合缺失集数并剔除已下载的集数
exist_flag, no_exists = self.__get_subscribe_no_exits(
subscribe_name=f'{subscribe.name} {meta.season}',
no_exists=no_exists,
mediakey=mediakey,
begin_season=meta.begin_season,
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode,
downloaded_episodes=downloaded
)
elif meta.type == MediaType.MOVIE:
# 对于电影类型,直接根据是否已下载判断
exist_flag = bool(downloaded)
exist_flag, no_exists = self.resolve_subscribe_missing(
subscribe=subscribe,
meta=meta,
mediainfo=mediainfo,
mediakey=mediakey,
)
# 如果已下载完毕,执行订阅完成操作
if exist_flag:
@@ -3146,6 +3130,113 @@ class SubscribeChain(ChainBase):
# 返回结果,表示媒体未完全下载或存在
return False, no_exists
def resolve_subscribe_missing(self, subscribe: Subscribe, meta: MetaBase,
mediainfo: MediaInfo,
mediakey: Optional[Union[str, int]] = None,
best_version_accept_downloaded: bool = False):
"""
按主程序订阅口径查询当前目标是否仍有缺失,不推进订阅状态。
该方法只组合媒体库缺集、订阅范围、下载历史和洗版优先级,用于外部策略在
完成前复用主程序"还要不要搜索/下载"的判断口径。它不得完成订阅、写入
lack_episode、发送事件或修改数据库。
best_version_accept_downloaded 仅用于分集洗版的外部完成守卫:为 True 时,
priority>0 的目标集视为已满足;默认 False 保持主程序洗版完成需 priority==100
的搜索/完成口径。
"""
mediakey = mediakey or subscribe.tmdbid or subscribe.doubanid
effective_total_episode = self.__resolve_effective_total_episode(subscribe, mediainfo)
if not subscribe.best_version:
totals = {}
if subscribe.season and effective_total_episode:
totals = {
subscribe.season: effective_total_episode
}
exist_flag, no_exists = DownloadChain().get_no_exists_info(
meta=meta,
mediainfo=mediainfo,
totals=totals
)
elif meta.type != MediaType.TV and self.__is_best_version_complete(subscribe):
return True, {}
else:
exist_flag = False
if meta.type == MediaType.TV:
if self.__is_full_best_version_enabled(subscribe):
pending_episodes = []
elif best_version_accept_downloaded:
downloaded = set(self.__get_downloaded_best_version_episodes(
subscribe, total_episode=effective_total_episode
))
start_episode = subscribe.start_episode or 1
pending_episodes = [
episode for episode in range(start_episode, effective_total_episode + 1)
if episode not in downloaded
]
if not pending_episodes:
return True, {}
else:
pending_episodes = self._get_pending_best_version_episodes(
subscribe, total_episode=effective_total_episode
)
if not pending_episodes:
return True, {}
no_exists = {
mediakey: {
subscribe.season: schemas.NotExistMediaInfo(
season=subscribe.season,
episodes=pending_episodes,
total_episode=effective_total_episode,
start_episode=subscribe.start_episode or 1,
require_complete_coverage=self.__is_full_best_version_enabled(subscribe))
}
}
else:
no_exists = {}
if exist_flag:
return True, no_exists
downloaded = self.__get_downloaded(subscribe)
if self.__is_full_best_version_enabled(subscribe):
downloaded = []
if meta.type == MediaType.TV:
return self.__get_subscribe_no_exits(
subscribe_name=f'{subscribe.name} {meta.season}',
no_exists=no_exists,
mediakey=mediakey,
begin_season=meta.begin_season,
total_episode=effective_total_episode,
start_episode=subscribe.start_episode,
downloaded_episodes=downloaded
)
if meta.type == MediaType.MOVIE:
return bool(downloaded), no_exists
return False, no_exists
@staticmethod
def __resolve_effective_total_episode(subscribe: Subscribe, mediainfo: MediaInfo) -> int:
"""
只读计算完成前有效总集数,不触发事件、不写回订阅。
主流程会通过 ``__refresh_total_episode_before_completion`` 持久化增长后的总集数;
该查询接口只需要同样避免旧 total 造成误判,因此仅使用当前 mediainfo 中更大的
季集数作为临时目标范围。
"""
current_total = subscribe.total_episode or 0
if subscribe.type != MediaType.TV.value:
return current_total
if subscribe.manual_total_episode:
return current_total
if subscribe.season is None:
return current_total
media_total = len((mediainfo.seasons or {}).get(subscribe.season) or [])
if media_total > current_total:
return media_total
return current_total
@staticmethod
def __apply_episodes_refresh(current_total: int, season: Optional[int], *,
mediainfo: Optional[MediaInfo] = None,