mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-07-13 08:24:36 +08:00
fix(subscribe): preserve confirmed episode floor (#6102)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user