revert: absolute numbered season pack locating (#5869)

This commit is contained in:
InfinityPacer
2026-06-01 21:09:23 +08:00
committed by GitHub
parent d353e7b208
commit e43adf51af
5 changed files with 49 additions and 748 deletions

View File

@@ -554,14 +554,6 @@ class DownloadChain(ChainBase):
effective &= set(allowed)
return effective
def __get_located_target_episodes(_context: Context) -> Set[int]:
"""
返回集数定位后的目标季内集数;缺失定位不扩大候选范围。
"""
if not _context.located_episodes:
return set()
return set(_context.located_episodes)
def __get_movie_download_key(_context: Context) -> str:
"""
获取电影下载去重键,确保失败候选不会阻断后续同名资源尝试。
@@ -640,9 +632,8 @@ class DownloadChain(ChainBase):
# 没有季的默认为第1季
if not torrent_season:
torrent_season = [1]
# 常规标题带集数的资源不走整季路径;已完成高置信集数定位的候选仍需打开种子文件确认。
located_target_episodes = __get_located_target_episodes(context)
if meta.episode_list and not located_target_episodes:
# 种子有集的不要
if meta.episode_list:
continue
# 匹配TMDBID
if need_mid == media.tmdb_id or need_mid == media.douban_id:
@@ -763,9 +754,8 @@ class DownloadChain(ChainBase):
# 只处理单季含集的种子
if len(torrent_season) != 1 or torrent_season[0] != need_season:
continue
# 种子集列表;累计总集编号等场景优先使用订阅阶段定位出的季内目标集。
located_target_episodes = __get_located_target_episodes(context)
torrent_episodes = located_target_episodes or set(meta.episode_list)
# 种子集列表
torrent_episodes = set(meta.episode_list)
# 整季的不处理
if not torrent_episodes:
continue
@@ -847,12 +837,10 @@ class DownloadChain(ChainBase):
effective_need = __apply_allowed_episodes(need_episodes, context)
if not effective_need:
continue
located_target_episodes = __get_located_target_episodes(context)
match_episode_candidates = located_target_episodes or set(meta.episode_list)
# 选中一个单季整季的或单季包括需要的所有集的
if (media.tmdb_id == need_mid or media.douban_id == need_mid) \
and (not match_episode_candidates
or match_episode_candidates.intersection(effective_need)) \
and (not meta.episode_list
or set(meta.episode_list).intersection(effective_need)) \
and len(meta.season_list) == 1 \
and meta.season_list[0] == need_season:
# 检查种子看是否有需要的集

View File

@@ -24,7 +24,7 @@ from app.helper.interaction import (
from app.chain.tmdb import TmdbChain
from app.chain.torrents import TorrentsChain
from app.core.config import settings, global_vars
from app.core.context import TorrentInfo, Context, MediaInfo, EpisodeLocation
from app.core.context import TorrentInfo, Context, MediaInfo
from app.core.event import eventmanager, Event
from app.core.meta import MetaBase
from app.core.meta.words import WordsMatcher
@@ -283,7 +283,6 @@ class SubscribeChain(ChainBase):
subscribe: Subscribe,
context: Context,
priority: int,
episode_location: Optional[EpisodeLocation] = None,
) -> List[int]:
"""
获取当前资源中仍值得继续洗版的剧集。
@@ -297,11 +296,7 @@ class SubscribeChain(ChainBase):
selected_episodes = getattr(context, "selected_episodes", None)
if selected_episodes is None:
if episode_location is None:
episode_location = cls.__locate_context_episodes(context=context, subscribe=subscribe)
selected_episodes = episode_location.target_episodes \
if cls.__is_high_confidence_episode_location(episode_location) \
else (context.meta_info.episode_list if context.meta_info else [])
selected_episodes = context.meta_info.episode_list if context.meta_info else []
if not selected_episodes:
episode_priority = cls.__get_episode_priority(subscribe)
return sorted([
@@ -323,59 +318,6 @@ class SubscribeChain(ChainBase):
interested.append(episode_num)
return sorted(set(interested))
@classmethod
def __normalize_best_version_resource_episodes(
cls,
meta: MetaBase,
subscribe: Subscribe,
episodes: Optional[List[int]] = None,
) -> List[int]:
"""
将同季累计总集编号映射为订阅季内集数。
"""
raw_episodes = episodes if episodes is not None else (meta.episode_list if meta else [])
if not raw_episodes:
return []
try:
resource_episodes = sorted(set(int(episode) for episode in raw_episodes))
except (TypeError, ValueError):
return list(raw_episodes)
episode_location = EpisodeLocation.locate(
meta=meta,
target_season=subscribe.season,
target_episodes=cls.__get_best_version_target_episodes(subscribe),
episodes=resource_episodes,
)
if episode_location:
return episode_location.target_episodes
return resource_episodes
@classmethod
def __locate_context_episodes(cls, context: Context, subscribe: Subscribe) -> Optional[EpisodeLocation]:
"""
为候选上下文缓存集数定位结果,供订阅过滤和下载链保持同一套集数语义。
"""
if not context or not context.meta_info:
return None
episode_location = context.locate_episode(
target_season=subscribe.season,
target_episodes=cls.__get_best_version_target_episodes(subscribe),
)
# located_episodes 是面向后续流程的目标季内集数视图,只接受可直接参与匹配的高置信定位结果。
context.located_episodes = set(episode_location.target_episodes) \
if episode_location and episode_location.confidence == EpisodeLocation.CONFIDENCE_HIGH else None
return episode_location
@staticmethod
def __is_high_confidence_episode_location(episode_location: Optional[EpisodeLocation]) -> bool:
"""
判断集数定位结果是否可直接参与订阅过滤与状态更新。
"""
return bool(episode_location and episode_location.confidence == EpisodeLocation.CONFIDENCE_HIGH)
@classmethod
def __is_full_best_version_enabled(cls, subscribe: Subscribe) -> bool:
"""
@@ -388,24 +330,17 @@ class SubscribeChain(ChainBase):
)
@classmethod
def __is_full_season_resource(
cls,
meta: MetaBase,
subscribe: Subscribe,
episode_location: Optional[EpisodeLocation] = None,
) -> bool:
def __is_full_season_resource(cls, meta: MetaBase, subscribe: Subscribe) -> bool:
"""
判断候选资源是否覆盖订阅目标全集范围。
"""
season_list = meta.season_list or [1]
if subscribe.season is not None:
if len(season_list) != 1:
return False
if season_list[0] != subscribe.season:
return False
if len(season_list) != 1:
return False
if subscribe.season is not None and season_list[0] != subscribe.season:
return False
episodes = episode_location.target_episodes if cls.__is_high_confidence_episode_location(episode_location) \
else cls.__normalize_best_version_resource_episodes(meta=meta, subscribe=subscribe)
episodes = meta.episode_list
if not episodes:
# 资源未标出单集时按整季包处理,后续下载前仍会解析种子文件确认完整性。
return True
@@ -416,23 +351,14 @@ class SubscribeChain(ChainBase):
return target_episodes.issubset(set(episodes))
@classmethod
def __is_full_season_best_version_resource(
cls,
meta: MetaBase,
subscribe: Subscribe,
episode_location: Optional[EpisodeLocation] = None,
) -> bool:
def __is_full_season_best_version_resource(cls, meta: MetaBase, subscribe: Subscribe) -> bool:
"""
判断候选资源是否符合全集洗版资源约束。
"""
if not cls.__is_full_best_version_enabled(subscribe):
return True
return cls.__is_full_season_resource(
meta=meta,
subscribe=subscribe,
episode_location=episode_location,
)
return cls.__is_full_season_resource(meta=meta, subscribe=subscribe)
@classmethod
def __is_full_season_priority_higher_than_all_targets(cls, subscribe: Subscribe, priority: int) -> bool:
@@ -1110,21 +1036,13 @@ class SubscribeChain(ChainBase):
torrent_meta = context.meta_info
torrent_info = context.torrent_info
torrent_mediainfo = context.media_info
episode_location = None
if torrent_mediainfo.type == MediaType.TV:
episode_location = self.__locate_context_episodes(
context=context,
subscribe=subscribe,
)
# 洗版
if subscribe.best_version:
if (
torrent_mediainfo.type == MediaType.TV
and not self.__is_full_season_best_version_resource(
meta=torrent_meta,
subscribe=subscribe,
episode_location=episode_location,
meta=torrent_meta, subscribe=subscribe
)
):
logger.info(
@@ -1135,9 +1053,7 @@ class SubscribeChain(ChainBase):
if (
torrent_mediainfo.type == MediaType.TV
and not self._is_episode_range_covered(
meta=torrent_meta,
subscribe=subscribe,
episode_location=episode_location,
meta=torrent_meta, subscribe=subscribe
)
):
logger.info(
@@ -1150,7 +1066,6 @@ class SubscribeChain(ChainBase):
subscribe=subscribe,
context=context,
priority=torrent_info.pri_order,
episode_location=episode_location,
)
if not interested_episodes:
logger.info(
@@ -1243,15 +1158,7 @@ class SubscribeChain(ChainBase):
updated = False
for download in downloads:
download_priority = download.torrent_info.pri_order
if download.located_episodes:
downloaded_episodes = sorted(download.located_episodes)
else:
downloaded_episodes = self.__get_downloaded_episodes([download])
downloaded_episodes = self.__normalize_best_version_resource_episodes(
meta=download.meta_info,
subscribe=subscribe,
episodes=downloaded_episodes,
)
downloaded_episodes = self.__get_downloaded_episodes([download])
if not downloaded_episodes and self.__is_full_season_resource(download.meta_info, subscribe):
# 整包下载时资源标题常不携带集数,视为覆盖当前订阅的全部目标集。
downloaded_episodes = self.__get_best_version_target_episodes(subscribe)
@@ -1612,7 +1519,6 @@ class SubscribeChain(ChainBase):
# 如果是电视剧
if torrent_mediainfo.type == MediaType.TV:
episode_location = self.__locate_context_episodes(context=_context, subscribe=subscribe)
# 有多季的不要
if len(torrent_meta.season_list) > 1:
logger.debug(f'{torrent_info.title} 有多季,不处理')
@@ -1632,24 +1538,20 @@ class SubscribeChain(ChainBase):
# 缺失集
no_exists_info = no_exists.get(mediakey).get(subscribe.season)
if no_exists_info:
torrent_episodes = episode_location.target_episodes \
if self.__is_high_confidence_episode_location(episode_location) \
else torrent_meta.episode_list
# 是否有交集
if no_exists_info.episodes and \
torrent_episodes and \
torrent_meta.episode_list and \
not set(no_exists_info.episodes).intersection(
set(torrent_episodes)
set(torrent_meta.episode_list)
):
logger.debug(
f'{torrent_info.title} 对应剧集 {torrent_episodes} 未包含缺失的剧集'
f'{torrent_info.title} 对应剧集 {torrent_meta.episode_list} 未包含缺失的剧集'
)
continue
else:
if not self.__is_full_season_best_version_resource(
meta=torrent_meta,
subscribe=subscribe,
episode_location=episode_location,
):
logger.debug(
f"{subscribe.name} 正在全集洗版,{torrent_info.title} 不是全集资源"
@@ -1661,7 +1563,6 @@ class SubscribeChain(ChainBase):
and not self._is_episode_range_covered(
meta=torrent_meta,
subscribe=subscribe,
episode_location=episode_location,
)
):
logger.debug(
@@ -1697,7 +1598,6 @@ class SubscribeChain(ChainBase):
subscribe=subscribe,
context=_context,
priority=torrent_info.pri_order,
episode_location=episode_location,
)
if not interested_episodes:
logger.info(
@@ -3328,17 +3228,11 @@ class SubscribeChain(ChainBase):
)
@classmethod
def _is_episode_range_covered(
cls,
meta: MetaBase,
subscribe: Subscribe,
episode_location: Optional[EpisodeLocation] = None,
) -> bool:
def _is_episode_range_covered(cls, meta: MetaBase, subscribe: Subscribe) -> bool:
"""
判断种子是否覆盖当前仍需洗版的剧集范围。
"""
episodes = episode_location.target_episodes if cls.__is_high_confidence_episode_location(episode_location) \
else cls.__normalize_best_version_resource_episodes(meta=meta, subscribe=subscribe)
episodes = meta.episode_list
if not episodes:
# 没有剧集信息,表示该种子为合集
return True

View File

@@ -1,7 +1,7 @@
import re
from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Dict, Any, Tuple, Optional, Set, ClassVar
from typing import List, Dict, Any, Tuple, Optional, Set
from app.core.config import settings
from app.core.meta import MetaBase
@@ -808,72 +808,6 @@ class MediaInfo:
self.episode_groups = []
@dataclass
class EpisodeLocation:
"""
候选资源标题集数与订阅目标季内集数之间的定位结果。
"""
# 定位模式absolute_to_season 表示同季累计总集编号映射为订阅季内集数。
MODE_ABSOLUTE_TO_SEASON: ClassVar[str] = "absolute_to_season"
# 置信度high 表示可直接用于订阅过滤和下载匹配low 仅作为未来种子文件确认前的弱提示预留。
CONFIDENCE_HIGH: ClassVar[str] = "high"
CONFIDENCE_LOW: ClassVar[str] = "low"
# 标题或副标题中解析出的原始集数,例如累计总集编号 57-82
source_episodes: List[int] = field(default_factory=list)
# 映射到订阅目标季内的集数,例如第 5 季的 1-26
target_episodes: List[int] = field(default_factory=list)
# 定位模式当前有效值absolute_to_season。
mode: str = MODE_ABSOLUTE_TO_SEASON
# 定位置信度当前消费值highlow 预留给只允许进入种子文件确认的弱定位。
confidence: str = CONFIDENCE_HIGH
@classmethod
def locate(
cls,
meta: MetaBase,
target_season: Optional[int],
target_episodes: List[int],
episodes: Optional[List[int]] = None,
) -> Optional["EpisodeLocation"]:
"""
识别候选标题集数与订阅目标季内集数之间的确定性映射关系。
"""
if not meta:
return None
raw_episodes = episodes if episodes is not None else meta.episode_list
if not raw_episodes or not target_episodes:
return None
try:
source_episodes = sorted(set(int(episode) for episode in raw_episodes))
except (TypeError, ValueError):
return None
if set(source_episodes).intersection(set(target_episodes)):
return None
season_list = meta.season_list or []
if target_season is None or len(season_list or []) != 1 or season_list[0] != target_season:
return None
if len(source_episodes) != len(target_episodes):
return None
if source_episodes != list(range(source_episodes[0], source_episodes[-1] + 1)):
return None
# 累计总集编号通常表现为 Sxx + 连续高位区间,例如 S05 的 57-82
# 只有当候选区间完整覆盖订阅目标长度时才定位,避免部分分集包被误当全集。
return cls(
source_episodes=source_episodes,
target_episodes=target_episodes,
mode=cls.MODE_ABSOLUTE_TO_SEASON,
confidence=cls.CONFIDENCE_HIGH,
)
@dataclass
class Context:
"""
@@ -898,22 +832,6 @@ class Context:
media_info_is_target: bool = False
# 调用方对本候选允许下载的剧集集合None 表示不限制,空集合表示拒绝交付任何集。
allowed_episodes: Optional[Set[int]] = None
# 调用方完成高置信集数定位后写入的目标季内集数集合None 表示未定位或不应放大候选范围。
located_episodes: Optional[Set[int]] = None
def locate_episode(
self,
target_season: Optional[int],
target_episodes: List[int],
) -> Optional[EpisodeLocation]:
"""
根据当前候选元数据和订阅目标范围计算集数定位结果。
"""
return EpisodeLocation.locate(
meta=self.meta_info,
target_season=target_season,
target_episodes=target_episodes,
)
def to_dict(self):
"""
@@ -930,5 +848,4 @@ class Context:
"media_info_is_target": self.media_info_is_target,
# 保留 None / 空集 / 非空集 三态语义,避免下游误把"显式拒绝"当成"不限制"。
"allowed_episodes": sorted(self.allowed_episodes) if self.allowed_episodes is not None else None,
"located_episodes": sorted(self.located_episodes) if self.located_episodes is not None else None,
}