mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-25 02:00:13 +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,
|
tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid,
|
||||||
subscribe_id=subscribe.id, scene="refresh")
|
subscribe_id=subscribe.id, scene="refresh")
|
||||||
old_total_episode = subscribe.total_episode or 0
|
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:
|
if total_episode and total_episode != old_total_episode:
|
||||||
progress_update = self.__prepare_total_episode_change_fields(
|
progress_update = self.__prepare_total_episode_change_fields(
|
||||||
subscribe=subscribe,
|
subscribe=subscribe,
|
||||||
@@ -3655,7 +3663,12 @@ class SubscribeChain(ChainBase):
|
|||||||
- exist_flag (bool): 布尔值,表示媒体是否已经完全下载或已存在
|
- exist_flag (bool): 布尔值,表示媒体是否已经完全下载或已存在
|
||||||
- no_exists (dict): 缺失的媒体信息,包含缺失的集数或其他相关信息
|
- 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(
|
exist_flag, no_exists = self.resolve_subscribe_missing(
|
||||||
subscribe=subscribe,
|
subscribe=subscribe,
|
||||||
@@ -3759,6 +3772,66 @@ class SubscribeChain(ChainBase):
|
|||||||
return bool(downloaded), no_exists
|
return bool(downloaded), no_exists
|
||||||
return False, 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
|
@staticmethod
|
||||||
def __resolve_effective_total_episode(subscribe: Subscribe, mediainfo: MediaInfo) -> int:
|
def __resolve_effective_total_episode(subscribe: Subscribe, mediainfo: MediaInfo) -> int:
|
||||||
"""
|
"""
|
||||||
@@ -3828,7 +3901,13 @@ class SubscribeChain(ChainBase):
|
|||||||
return result.total_episode
|
return result.total_episode
|
||||||
return current_total
|
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,
|
tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid,
|
||||||
subscribe_id=subscribe.id, scene="precheck")
|
subscribe_id=subscribe.id, scene="precheck")
|
||||||
old_total_episode = subscribe.total_episode or 0
|
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:
|
if not new_total_episode or new_total_episode == old_total_episode:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1004,6 +1004,25 @@ class SubscribeChainTest(TestCase):
|
|||||||
self.assertTrue(satisfied)
|
self.assertTrue(satisfied)
|
||||||
self.assertEqual(no_exists, {})
|
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):
|
def test_resolve_subscribe_missing_accepts_downloaded_legacy_current_priority_targets(self):
|
||||||
"""外部完成守卫读取按集事实时,应保留 current_priority 整体快照兼容。"""
|
"""外部完成守卫读取按集事实时,应保留 current_priority 整体快照兼容。"""
|
||||||
subscribe = self._build_subscribe(
|
subscribe = self._build_subscribe(
|
||||||
@@ -1149,7 +1168,7 @@ class SubscribeChainTest(TestCase):
|
|||||||
mediainfo = SimpleNamespace(type=MediaType.TV, title_year="Test Show (2026)")
|
mediainfo = SimpleNamespace(type=MediaType.TV, title_year="Test Show (2026)")
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
def fake_refresh(_self, subscribe, mediainfo):
|
def fake_refresh(_self, subscribe, mediainfo, meta=None, mediakey=None):
|
||||||
calls.append(("refresh", subscribe.total_episode))
|
calls.append(("refresh", subscribe.total_episode))
|
||||||
subscribe.total_episode = 20
|
subscribe.total_episode = 20
|
||||||
|
|
||||||
@@ -2548,7 +2567,7 @@ class SubscribeProgressConsolidationTest(TestCase):
|
|||||||
self.assertEqual(updates[-1][1]["lack_episode"], 2)
|
self.assertEqual(updates[-1][1]["lack_episode"], 2)
|
||||||
self.assertEqual(updates[-1][1]["current_priority"], 0)
|
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()
|
module, SubscribeChain = _load_subscribe_chain_class()
|
||||||
subscribe = module.Subscribe(
|
subscribe = module.Subscribe(
|
||||||
id=34,
|
id=34,
|
||||||
@@ -2558,10 +2577,10 @@ class SubscribeProgressConsolidationTest(TestCase):
|
|||||||
total_episode=100,
|
total_episode=100,
|
||||||
start_episode=1,
|
start_episode=1,
|
||||||
lack_episode=100,
|
lack_episode=100,
|
||||||
best_version=0,
|
best_version=1,
|
||||||
best_version_full=0,
|
best_version_full=0,
|
||||||
current_priority=None,
|
current_priority=None,
|
||||||
episode_priority={},
|
episode_priority={str(episode): 80 for episode in range(1, 101)},
|
||||||
note=[],
|
note=[],
|
||||||
tmdbid=31034,
|
tmdbid=31034,
|
||||||
doubanid=None,
|
doubanid=None,
|
||||||
@@ -2574,21 +2593,31 @@ class SubscribeProgressConsolidationTest(TestCase):
|
|||||||
def update(self, subscribe_id, payload):
|
def update(self, subscribe_id, payload):
|
||||||
updates.append((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(
|
with patch.object(module, "SubscribeOper", return_value=_SubscribeOper()), patch.object(
|
||||||
module,
|
module,
|
||||||
"eventmanager",
|
"eventmanager",
|
||||||
eventmanager,
|
eventmanager,
|
||||||
):
|
):
|
||||||
SubscribeChain()._SubscribeChain__refresh_total_episode_before_completion(
|
chain._SubscribeChain__refresh_total_episode_before_completion(
|
||||||
subscribe,
|
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(captured[0][1].current_total_episode, 1)
|
||||||
self.assertEqual(subscribe.total_episode, 90)
|
self.assertEqual(subscribe.total_episode, 100)
|
||||||
self.assertEqual(subscribe.lack_episode, 90)
|
self.assertEqual(updates, [])
|
||||||
self.assertEqual(updates[-1][1]["total_episode"], 90)
|
self.assertTrue(resolve_calls[0]["best_version_accept_downloaded"])
|
||||||
self.assertEqual(updates[-1][1]["lack_episode"], 90)
|
|
||||||
|
|
||||||
def test_refresh_total_episode_before_completion_filters_best_version_priority_on_decrease(self):
|
def test_refresh_total_episode_before_completion_filters_best_version_priority_on_decrease(self):
|
||||||
module, SubscribeChain = _load_subscribe_chain_class()
|
module, SubscribeChain = _load_subscribe_chain_class()
|
||||||
@@ -2950,7 +2979,7 @@ class SubscribeProgressConsolidationTest(TestCase):
|
|||||||
self.assertEqual(payload["total_episode"], 120)
|
self.assertEqual(payload["total_episode"], 120)
|
||||||
self.assertEqual(payload["lack_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()
|
module, SubscribeChain = _load_subscribe_chain_class()
|
||||||
subscribe = module.Subscribe(
|
subscribe = module.Subscribe(
|
||||||
id=43,
|
id=43,
|
||||||
@@ -2982,7 +3011,21 @@ class SubscribeProgressConsolidationTest(TestCase):
|
|||||||
updates.append((subscribe_id, payload))
|
updates.append((subscribe_id, payload))
|
||||||
|
|
||||||
chain = SubscribeChain()
|
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(
|
with patch.object(module, "SubscribeOper", return_value=_SubscribeOper()), patch.object(
|
||||||
module,
|
module,
|
||||||
@@ -2992,7 +3035,7 @@ class SubscribeProgressConsolidationTest(TestCase):
|
|||||||
chain.check()
|
chain.check()
|
||||||
|
|
||||||
payload = updates[-1][1]
|
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["total_episode"], 90)
|
||||||
self.assertEqual(payload["lack_episode"], 90)
|
self.assertEqual(payload["lack_episode"], 90)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user