fix(subscribe): preserve confirmed episode floor (#6102)

This commit is contained in:
InfinityPacer
2026-07-12 07:18:29 +08:00
committed by GitHub
parent 54be1143fc
commit 30b932e07e
2 changed files with 146 additions and 16 deletions

View File

@@ -1993,6 +1993,14 @@ class SubscribeChain(ChainBase):
tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid,
subscribe_id=subscribe.id, scene="refresh")
old_total_episode = subscribe.total_episode or 0
if total_episode and total_episode < old_total_episode:
total_episode = self.__resolve_total_episode_decrease(
subscribe=subscribe,
candidate_total=total_episode,
meta=meta,
mediainfo=mediainfo,
mediakey=subscribe.tmdbid or subscribe.doubanid,
)
if total_episode and total_episode != old_total_episode:
progress_update = self.__prepare_total_episode_change_fields(
subscribe=subscribe,
@@ -3655,7 +3663,12 @@ class SubscribeChain(ChainBase):
- exist_flag (bool): 布尔值,表示媒体是否已经完全下载或已存在
- no_exists (dict): 缺失的媒体信息,包含缺失的集数或其他相关信息
"""
self.__refresh_total_episode_before_completion(subscribe=subscribe, mediainfo=mediainfo)
self.__refresh_total_episode_before_completion(
subscribe=subscribe,
mediainfo=mediainfo,
meta=meta,
mediakey=mediakey,
)
exist_flag, no_exists = self.resolve_subscribe_missing(
subscribe=subscribe,
@@ -3759,6 +3772,66 @@ class SubscribeChain(ChainBase):
return bool(downloaded), no_exists
return False, no_exists
def __resolve_total_episode_decrease(
self,
subscribe: Subscribe,
candidate_total: int,
meta: MetaBase,
mediainfo: MediaInfo,
mediakey: Optional[Union[str, int]] = None,
) -> int:
"""以旧目标范围内已确认存在的最高集号限制总集数回落。"""
old_total = subscribe.total_episode or 0
if candidate_total >= old_total or not old_total:
return candidate_total
if subscribe.type != MediaType.TV.value or self.__is_full_best_version_enabled(subscribe):
return candidate_total
target_key = mediakey or subscribe.tmdbid or subscribe.doubanid
target_season = subscribe.season
target_start = subscribe.start_episode or 1
snapshot = copy.copy(subscribe)
snapshot.total_episode = old_total
try:
satisfied, no_exists = self.resolve_subscribe_missing(
subscribe=snapshot,
meta=meta,
mediainfo=mediainfo,
mediakey=target_key,
best_version_accept_downloaded=bool(subscribe.best_version),
)
except Exception as err:
logger.warning(f"订阅 {subscribe.name} 已存在分集事实查询失败,按元数据总集数继续:{err}")
return candidate_total
if satisfied:
return old_total
if not isinstance(no_exists, dict):
return candidate_total
seasons = no_exists.get(target_key)
if not isinstance(seasons, dict):
return candidate_total
missing_info = seasons.get(target_season)
if not missing_info:
return candidate_total
try:
scope_matches = missing_info.season == target_season \
and missing_info.start_episode == target_start \
and missing_info.total_episode == old_total
episodes = missing_info.episodes
except AttributeError:
return candidate_total
if not scope_matches:
return candidate_total
if not isinstance(episodes, list) or not episodes:
return candidate_total
if any(isinstance(episode, bool) or not isinstance(episode, int)
or episode < target_start or episode > old_total for episode in episodes):
return candidate_total
confirmed = set(range(target_start, old_total + 1)).difference(episodes)
return max(candidate_total, max(confirmed) if confirmed else 0)
@staticmethod
def __resolve_effective_total_episode(subscribe: Subscribe, mediainfo: MediaInfo) -> int:
"""
@@ -3828,7 +3901,13 @@ class SubscribeChain(ChainBase):
return result.total_episode
return current_total
def __refresh_total_episode_before_completion(self, subscribe: Subscribe, mediainfo: MediaInfo):
def __refresh_total_episode_before_completion(
self,
subscribe: Subscribe,
mediainfo: MediaInfo,
meta: Optional[MetaBase] = None,
mediakey: Optional[Union[str, int]] = None,
) -> None:
"""
在完成判断前,按最新识别结果兜底修正订阅总集数,防止旧总集数导致误完成。
"""
@@ -3846,6 +3925,14 @@ class SubscribeChain(ChainBase):
tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid,
subscribe_id=subscribe.id, scene="precheck")
old_total_episode = subscribe.total_episode or 0
if meta is not None and new_total_episode and new_total_episode < old_total_episode:
new_total_episode = self.__resolve_total_episode_decrease(
subscribe=subscribe,
candidate_total=new_total_episode,
meta=meta,
mediainfo=mediainfo,
mediakey=mediakey,
)
if not new_total_episode or new_total_episode == old_total_episode:
return

View File

@@ -1004,6 +1004,25 @@ class SubscribeChainTest(TestCase):
self.assertTrue(satisfied)
self.assertEqual(no_exists, {})
def test_total_episode_decrease_rejects_invalid_missing_scope(self):
subscribe = self._build_subscribe(best_version=0, total_episode=100, note=[])
missing_info = SimpleNamespace(
episodes=list(range(91, 101)),
require_complete_coverage=False,
)
chain = SubscribeChain()
with patch.object(chain, "resolve_subscribe_missing", return_value=(False, {1: {1: missing_info}})):
total_episode = chain._SubscribeChain__resolve_total_episode_decrease(
subscribe=subscribe,
candidate_total=1,
meta=SimpleNamespace(type=MediaType.TV, begin_season=1, season=1),
mediainfo=SimpleNamespace(type=MediaType.TV, seasons={1: [1]}),
mediakey=1,
)
self.assertEqual(total_episode, 1)
def test_resolve_subscribe_missing_accepts_downloaded_legacy_current_priority_targets(self):
"""外部完成守卫读取按集事实时,应保留 current_priority 整体快照兼容。"""
subscribe = self._build_subscribe(
@@ -1149,7 +1168,7 @@ class SubscribeChainTest(TestCase):
mediainfo = SimpleNamespace(type=MediaType.TV, title_year="Test Show (2026)")
calls = []
def fake_refresh(_self, subscribe, mediainfo):
def fake_refresh(_self, subscribe, mediainfo, meta=None, mediakey=None):
calls.append(("refresh", subscribe.total_episode))
subscribe.total_episode = 20
@@ -2548,7 +2567,7 @@ class SubscribeProgressConsolidationTest(TestCase):
self.assertEqual(updates[-1][1]["lack_episode"], 2)
self.assertEqual(updates[-1][1]["current_priority"], 0)
def test_refresh_total_episode_before_completion_follows_recognized_total_decrease(self):
def test_refresh_total_episode_before_completion_keeps_downloaded_best_version_floor(self):
module, SubscribeChain = _load_subscribe_chain_class()
subscribe = module.Subscribe(
id=34,
@@ -2558,10 +2577,10 @@ class SubscribeProgressConsolidationTest(TestCase):
total_episode=100,
start_episode=1,
lack_episode=100,
best_version=0,
best_version=1,
best_version_full=0,
current_priority=None,
episode_priority={},
episode_priority={str(episode): 80 for episode in range(1, 101)},
note=[],
tmdbid=31034,
doubanid=None,
@@ -2574,21 +2593,31 @@ class SubscribeProgressConsolidationTest(TestCase):
def update(self, subscribe_id, payload):
updates.append((subscribe_id, payload))
chain = SubscribeChain()
resolve_calls = []
def _resolve_missing(**kwargs):
resolve_calls.append(kwargs)
return True, {}
chain.resolve_subscribe_missing = _resolve_missing
with patch.object(module, "SubscribeOper", return_value=_SubscribeOper()), patch.object(
module,
"eventmanager",
eventmanager,
):
SubscribeChain()._SubscribeChain__refresh_total_episode_before_completion(
chain._SubscribeChain__refresh_total_episode_before_completion(
subscribe,
self._mediainfo(total_episode=90),
self._mediainfo(total_episode=1),
meta=SimpleNamespace(type=MediaType.TV, begin_season=1, season=1),
mediakey=31034,
)
self.assertEqual(captured[0][1].current_total_episode, 90)
self.assertEqual(subscribe.total_episode, 90)
self.assertEqual(subscribe.lack_episode, 90)
self.assertEqual(updates[-1][1]["total_episode"], 90)
self.assertEqual(updates[-1][1]["lack_episode"], 90)
self.assertEqual(captured[0][1].current_total_episode, 1)
self.assertEqual(subscribe.total_episode, 100)
self.assertEqual(updates, [])
self.assertTrue(resolve_calls[0]["best_version_accept_downloaded"])
def test_refresh_total_episode_before_completion_filters_best_version_priority_on_decrease(self):
module, SubscribeChain = _load_subscribe_chain_class()
@@ -2950,7 +2979,7 @@ class SubscribeProgressConsolidationTest(TestCase):
self.assertEqual(payload["total_episode"], 120)
self.assertEqual(payload["lack_episode"], 120)
def test_check_total_refresh_follows_recognized_total_decrease(self):
def test_check_total_refresh_uses_confirmed_episode_floor(self):
module, SubscribeChain = _load_subscribe_chain_class()
subscribe = module.Subscribe(
id=43,
@@ -2982,7 +3011,21 @@ class SubscribeProgressConsolidationTest(TestCase):
updates.append((subscribe_id, payload))
chain = SubscribeChain()
chain.recognize_media = lambda **kwargs: self._mediainfo(total_episode=90)
chain.recognize_media = lambda **kwargs: self._mediainfo(total_episode=1)
chain.resolve_subscribe_missing = lambda **kwargs: (
False,
{
31043: {
1: SimpleNamespace(
season=1,
episodes=list(range(91, 101)),
total_episode=100,
start_episode=1,
require_complete_coverage=False,
)
}
},
)
with patch.object(module, "SubscribeOper", return_value=_SubscribeOper()), patch.object(
module,
@@ -2992,7 +3035,7 @@ class SubscribeProgressConsolidationTest(TestCase):
chain.check()
payload = updates[-1][1]
self.assertEqual(captured[0][1].current_total_episode, 90)
self.assertEqual(captured[0][1].current_total_episode, 1)
self.assertEqual(payload["total_episode"], 90)
self.assertEqual(payload["lack_episode"], 90)