mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-11 02:31:25 +08:00
revert: absolute numbered season pack locating (#5869)
This commit is contained in:
@@ -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:
|
||||
# 检查种子看是否有需要的集
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# 定位置信度,当前消费值:high;low 预留给只允许进入种子文件确认的弱定位。
|
||||
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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user