mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-09 23:44:20 +08:00
fix(subscribe): refine TV wash priority lifecycle (#6153)
This commit is contained in:
@@ -281,6 +281,9 @@ def _load_subscribe_chain_class():
|
||||
|
||||
helper_server_module.MoviePilotServerHelper = _MoviePilotServerHelper
|
||||
|
||||
helper_mediaserver_module = ensure_module("app.helper.mediaserver", types.ModuleType("app.helper.mediaserver"))
|
||||
helper_mediaserver_module.MediaServerHelper = type("MediaServerHelper", (), {})
|
||||
|
||||
helper_torrent_module = ensure_module("app.helper.torrent", types.ModuleType("app.helper.torrent"))
|
||||
helper_torrent_module.TorrentHelper = type("TorrentHelper", (), {})
|
||||
|
||||
@@ -288,6 +291,9 @@ def _load_subscribe_chain_class():
|
||||
|
||||
class _SubscribeModel:
|
||||
def __init__(self, **kwargs):
|
||||
self.best_version_full = 0
|
||||
self.bangumiid = None
|
||||
self.episode_group = None
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
@@ -338,6 +344,7 @@ def _load_subscribe_chain_class():
|
||||
chain_dependencies = {
|
||||
"app.chain.download": "DownloadChain",
|
||||
"app.chain.media": "MediaChain",
|
||||
"app.chain.mediaserver": "MediaServerChain",
|
||||
"app.chain.search": "SearchChain",
|
||||
"app.chain.tmdb": "TmdbChain",
|
||||
"app.chain.torrents": "TorrentsChain",
|
||||
@@ -403,6 +410,7 @@ class SubscribeChainTest(TestCase):
|
||||
selected_episodes=selected_episodes,
|
||||
meta_info=SimpleNamespace(season_list=[1], episode_list=meta_episodes or selected_episodes or []),
|
||||
media_info=SimpleNamespace(type=MediaType.TV, tmdb_id=1, douban_id=None),
|
||||
confirmed_full_coverage=False,
|
||||
)
|
||||
|
||||
def test_default_kwargs_respects_explicit_zero_best_version(self):
|
||||
@@ -546,6 +554,7 @@ class SubscribeChainTest(TestCase):
|
||||
context = SimpleNamespace(
|
||||
media_info=torrent_media,
|
||||
media_recognize_fail_count=0,
|
||||
match_source="unknown",
|
||||
meta_info=SimpleNamespace(
|
||||
begin_season=0,
|
||||
episode_list=[1],
|
||||
@@ -607,6 +616,16 @@ class SubscribeChainTest(TestCase):
|
||||
{"1": 80, "2": 80, "3": 80},
|
||||
)
|
||||
|
||||
def test_full_best_version_does_not_materialize_episode_priority_from_baseline(self):
|
||||
subscribe = self._build_subscribe(
|
||||
best_version_full=1,
|
||||
current_priority=82,
|
||||
episode_priority=None,
|
||||
)
|
||||
|
||||
self.assertEqual(SubscribeChain.get_episode_priority(subscribe), {})
|
||||
self.assertEqual(SubscribeChain.get_best_version_current_priority(subscribe), 82)
|
||||
|
||||
def test_get_pending_best_version_episodes_uses_per_episode_status(self):
|
||||
subscribe = self._build_subscribe(
|
||||
total_episode=5,
|
||||
@@ -1427,8 +1446,8 @@ class SubscribeChainTest(TestCase):
|
||||
self.assertEqual(calls[0]["contexts"], [episode_context, full_pack_context])
|
||||
self.assertIs(calls[0]["no_exists"], no_exists)
|
||||
|
||||
def test_episode_best_version_tries_full_pack_when_priority_exceeds_current_priority(self):
|
||||
"""整包候选按 current_priority 整体门槛判断,单集较高优先级不阻断整包优先。"""
|
||||
def test_episode_best_version_falls_back_when_full_pack_does_not_exceed_every_target(self):
|
||||
"""整包候选未严格高于每个目标集时,应回退到按集下载。"""
|
||||
subscribe = self._build_subscribe(
|
||||
best_version_full=0,
|
||||
total_episode=3,
|
||||
@@ -1470,66 +1489,117 @@ class SubscribeChainTest(TestCase):
|
||||
|
||||
self.assertEqual(downloads, [])
|
||||
self.assertIs(lefts, no_exists)
|
||||
self.assertEqual(len(calls), 2)
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(calls[0]["contexts"], [full_pack_context])
|
||||
self.assertEqual(calls[0]["no_exists"]["media-key"][1].episodes, [])
|
||||
self.assertIs(calls[1]["no_exists"], no_exists)
|
||||
self.assertIs(calls[0]["no_exists"], no_exists)
|
||||
|
||||
def test_episode_full_pack_priority_must_strictly_exceed_all_targets(self):
|
||||
"""缺失集按 0 参与比较,且候选与任一目标集相等时也不优先整包。"""
|
||||
subscribe = self._build_subscribe(
|
||||
best_version_full=0,
|
||||
total_episode=3,
|
||||
episode_priority={"1": 90, "2": 80},
|
||||
current_priority=0,
|
||||
)
|
||||
|
||||
self.assertFalse(
|
||||
SubscribeChain._SubscribeChain__should_prefer_full_pack_for_episode_best_version(
|
||||
subscribe=subscribe,
|
||||
priority=82,
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
SubscribeChain._SubscribeChain__should_prefer_full_pack_for_episode_best_version(
|
||||
subscribe=subscribe,
|
||||
priority=90,
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
SubscribeChain._SubscribeChain__should_prefer_full_pack_for_episode_best_version(
|
||||
subscribe=subscribe,
|
||||
priority=91,
|
||||
)
|
||||
)
|
||||
|
||||
def test_full_pack_priority_check_uses_current_priority_fallback(self):
|
||||
"""没有按集优先级状态时使用 current_priority 兜底判断。"""
|
||||
subscribe = self._build_subscribe(total_episode=3, current_priority=80, episode_priority=None)
|
||||
|
||||
self.assertFalse(
|
||||
SubscribeChain._SubscribeChain__is_full_season_priority_higher_than_all_targets(
|
||||
SubscribeChain._SubscribeChain__should_prefer_full_pack_for_episode_best_version(
|
||||
subscribe=subscribe,
|
||||
priority=80,
|
||||
)
|
||||
)
|
||||
|
||||
def test_full_best_version_priority_check_uses_current_priority_directly(self):
|
||||
"""全集洗版优先判断使用 current_priority,避免部分 episode_priority 破坏整体优先级语义。"""
|
||||
def test_full_best_version_completion_uses_baseline_priority(self):
|
||||
"""全集洗版完成只认整包准入基线,不受保留的按集事实影响。"""
|
||||
subscribe = self._build_subscribe(
|
||||
best_version_full=1,
|
||||
total_episode=3,
|
||||
current_priority=82,
|
||||
episode_priority={"1": 100, "2": 100, "3": 100},
|
||||
)
|
||||
|
||||
self.assertFalse(SubscribeChain.is_best_version_complete(subscribe))
|
||||
subscribe.current_priority = 100
|
||||
subscribe.episode_priority = {"1": 80}
|
||||
self.assertTrue(SubscribeChain.is_best_version_complete(subscribe))
|
||||
|
||||
def test_full_best_version_candidate_uses_baseline_without_allowed_episodes(self):
|
||||
"""全集候选只比较整包准入基线,不进入按集 interested/allowed_episodes 路径。"""
|
||||
subscribe = self._build_subscribe(
|
||||
best_version_full=1,
|
||||
current_priority=82,
|
||||
episode_priority={"1": 100, "2": 100, "3": 100},
|
||||
)
|
||||
context = SimpleNamespace(
|
||||
selected_episodes=None,
|
||||
meta_info=SimpleNamespace(episode_list=[1, 2, 3]),
|
||||
allowed_episodes=None,
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
SubscribeChain,
|
||||
"_SubscribeChain__get_best_version_interested_episodes",
|
||||
side_effect=AssertionError("full mode must not inspect episode priorities"),
|
||||
):
|
||||
self.assertFalse(
|
||||
SubscribeChain._SubscribeChain__prepare_best_version_tv_candidate(
|
||||
subscribe=subscribe,
|
||||
context=context,
|
||||
priority=82,
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
SubscribeChain._SubscribeChain__prepare_best_version_tv_candidate(
|
||||
subscribe=subscribe,
|
||||
context=context,
|
||||
priority=83,
|
||||
)
|
||||
)
|
||||
|
||||
self.assertIsNone(context.allowed_episodes)
|
||||
|
||||
def test_episode_best_version_candidate_sets_allowed_episodes(self):
|
||||
"""分集候选继续把实际可提升剧集下传到下载层。"""
|
||||
subscribe = self._build_subscribe(
|
||||
best_version_full=0,
|
||||
current_priority=80,
|
||||
episode_priority={"1": 100},
|
||||
episode_priority={"1": 90, "2": 80},
|
||||
)
|
||||
context = SimpleNamespace(
|
||||
selected_episodes=None,
|
||||
meta_info=SimpleNamespace(episode_list=[1, 2, 3]),
|
||||
)
|
||||
|
||||
self.assertFalse(
|
||||
SubscribeChain._SubscribeChain__is_full_season_priority_higher_than_all_targets(
|
||||
subscribe=subscribe,
|
||||
priority=80,
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
SubscribeChain._SubscribeChain__is_full_season_priority_higher_than_all_targets(
|
||||
subscribe=subscribe,
|
||||
priority=81,
|
||||
)
|
||||
)
|
||||
subscribe.current_priority = 0
|
||||
self.assertTrue(
|
||||
SubscribeChain._SubscribeChain__is_full_season_priority_higher_than_all_targets(
|
||||
subscribe=subscribe,
|
||||
priority=10,
|
||||
)
|
||||
accepted = SubscribeChain._SubscribeChain__prepare_best_version_tv_candidate(
|
||||
subscribe=subscribe,
|
||||
context=context,
|
||||
priority=85,
|
||||
)
|
||||
|
||||
def test_full_pack_priority_check_computes_gate_when_current_priority_is_none(self):
|
||||
"""没有持久 current_priority 时,整包门槛按当前按集事实计算。"""
|
||||
subscribe = self._build_subscribe(
|
||||
best_version_full=1,
|
||||
total_episode=3,
|
||||
current_priority=None,
|
||||
episode_priority={"1": 100, "2": 80},
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
SubscribeChain._SubscribeChain__is_full_season_priority_higher_than_all_targets(
|
||||
subscribe=subscribe,
|
||||
priority=1,
|
||||
)
|
||||
)
|
||||
self.assertTrue(accepted)
|
||||
self.assertEqual(context.allowed_episodes, {2, 3})
|
||||
|
||||
def test_record_download_facts_uses_selected_episodes(self):
|
||||
subscribe = self._build_subscribe(
|
||||
@@ -2337,6 +2407,31 @@ class SubscribeProgressEntrypointTest(TestCase):
|
||||
self.assertEqual(subscribe.current_priority, 0)
|
||||
self.assertEqual(updates[-1]["current_priority"], 0)
|
||||
|
||||
def test_refresh_full_best_version_progress_preserves_baseline_priority(self):
|
||||
"""全集洗版刷新缺集进度时,不从按集事实覆盖整包准入基线。"""
|
||||
subscribe = self._build_subscribe(
|
||||
best_version_full=1,
|
||||
total_episode=3,
|
||||
current_priority=82,
|
||||
episode_priority={"1": 100},
|
||||
lack_episode=0,
|
||||
)
|
||||
updates = []
|
||||
|
||||
class _SubscribeOper:
|
||||
def update(self, subscribe_id, payload):
|
||||
updates.append(payload)
|
||||
|
||||
with patch.object(self.module, "SubscribeOper", return_value=_SubscribeOper()):
|
||||
self.SubscribeChain()._SubscribeChain__refresh_subscribe_progress_with_no_exists(
|
||||
subscribe=subscribe,
|
||||
no_exists={},
|
||||
scene="unit",
|
||||
)
|
||||
|
||||
self.assertEqual(subscribe.current_priority, 82)
|
||||
self.assertNotIn("current_priority", updates[-1])
|
||||
|
||||
def test_refresh_subscribe_progress_normal_tv_uses_resolve_missing_successfully(self):
|
||||
subscribe = self._build_subscribe(best_version=0, lack_episode=5)
|
||||
mediainfo = SimpleNamespace(
|
||||
@@ -2667,6 +2762,94 @@ class SubscribeProgressConsolidationTest(TestCase):
|
||||
list(range(1, 11)),
|
||||
)
|
||||
|
||||
def test_full_best_version_total_expansion_resets_baseline_priority(self):
|
||||
module, SubscribeChain = _load_subscribe_chain_class()
|
||||
subscribe = module.Subscribe(
|
||||
id=41,
|
||||
name="全集洗版扩展剧",
|
||||
type=MediaType.TV.value,
|
||||
season=1,
|
||||
total_episode=3,
|
||||
start_episode=1,
|
||||
lack_episode=0,
|
||||
best_version=1,
|
||||
best_version_full=1,
|
||||
current_priority=82,
|
||||
episode_priority={"1": 90, "2": 80, "3": 82},
|
||||
note=[1, 2, 3],
|
||||
tmdbid=31041,
|
||||
doubanid=None,
|
||||
manual_total_episode=0,
|
||||
)
|
||||
|
||||
payload = SubscribeChain._SubscribeChain__prepare_best_version_total_change_fields(
|
||||
subscribe=subscribe,
|
||||
total_episode=5,
|
||||
old_total_episode=3,
|
||||
)
|
||||
|
||||
self.assertEqual(subscribe.current_priority, 0)
|
||||
self.assertEqual(payload["current_priority"], 0)
|
||||
self.assertEqual(subscribe.episode_priority, {"1": 90, "2": 80, "3": 82})
|
||||
|
||||
def test_full_best_version_total_expansion_does_not_derive_episode_map_from_baseline(self):
|
||||
module, SubscribeChain = _load_subscribe_chain_class()
|
||||
subscribe = module.Subscribe(
|
||||
id=45,
|
||||
name="全集洗版无按集事实扩展剧",
|
||||
type=MediaType.TV.value,
|
||||
season=1,
|
||||
total_episode=3,
|
||||
start_episode=1,
|
||||
lack_episode=0,
|
||||
best_version=1,
|
||||
best_version_full=1,
|
||||
current_priority=82,
|
||||
episode_priority={},
|
||||
note=[],
|
||||
tmdbid=31045,
|
||||
doubanid=None,
|
||||
manual_total_episode=0,
|
||||
)
|
||||
|
||||
payload = SubscribeChain._SubscribeChain__prepare_best_version_total_expansion_fields(
|
||||
subscribe=subscribe,
|
||||
total_episode=5,
|
||||
)
|
||||
|
||||
self.assertEqual(subscribe.current_priority, 0)
|
||||
self.assertEqual(payload["episode_priority"], {})
|
||||
|
||||
def test_full_best_version_total_shrink_preserves_baseline_priority(self):
|
||||
module, SubscribeChain = _load_subscribe_chain_class()
|
||||
subscribe = module.Subscribe(
|
||||
id=43,
|
||||
name="全集洗版缩小剧",
|
||||
type=MediaType.TV.value,
|
||||
season=1,
|
||||
total_episode=5,
|
||||
start_episode=1,
|
||||
lack_episode=0,
|
||||
best_version=1,
|
||||
best_version_full=1,
|
||||
current_priority=82,
|
||||
episode_priority={"1": 90, "2": 80, "3": 82, "4": 70, "5": 60},
|
||||
note=[1, 2, 3, 4, 5],
|
||||
tmdbid=31043,
|
||||
doubanid=None,
|
||||
manual_total_episode=0,
|
||||
)
|
||||
|
||||
payload = SubscribeChain._SubscribeChain__prepare_best_version_total_change_fields(
|
||||
subscribe=subscribe,
|
||||
total_episode=3,
|
||||
old_total_episode=5,
|
||||
)
|
||||
|
||||
self.assertEqual(subscribe.current_priority, 82)
|
||||
self.assertEqual(payload["current_priority"], 82)
|
||||
self.assertEqual(subscribe.episode_priority, {"1": 90, "2": 80, "3": 82})
|
||||
|
||||
def test_refresh_total_episode_before_completion_resets_legacy_current_priority_when_filtered_empty(self):
|
||||
module, SubscribeChain = _load_subscribe_chain_class()
|
||||
subscribe = module.Subscribe(
|
||||
@@ -3163,6 +3346,23 @@ class SubscribeProgressConsolidationTest(TestCase):
|
||||
self.assertEqual(schema_subscribe.completed_episode, 3)
|
||||
self.assertEqual(schemas.compute_subscribe_completed_episode(chain_subscribe), 3)
|
||||
|
||||
def test_full_best_version_completed_episode_uses_baseline_not_episode_map(self):
|
||||
data = {
|
||||
"name": "全集洗版响应剧",
|
||||
"type": MediaType.TV.value,
|
||||
"best_version": 1,
|
||||
"best_version_full": 1,
|
||||
"start_episode": 1,
|
||||
"total_episode": 3,
|
||||
"current_priority": 82,
|
||||
"episode_priority": {"1": 100, "2": 100, "3": 100},
|
||||
}
|
||||
|
||||
self.assertEqual(schemas.compute_subscribe_completed_episode(SimpleNamespace(**data)), 0)
|
||||
data["current_priority"] = 100
|
||||
data["episode_priority"] = {"1": 80}
|
||||
self.assertEqual(schemas.compute_subscribe_completed_episode(SimpleNamespace(**data)), 3)
|
||||
|
||||
|
||||
class SubscribeDownloadFactsTest(TestCase):
|
||||
def setUp(self):
|
||||
@@ -3291,7 +3491,27 @@ class SubscribeDownloadFactsTest(TestCase):
|
||||
self.assertEqual(snapshot["episodes"], [1, 2, 3, 4])
|
||||
self.assertEqual(subscribe.note, [1, 2, 3, 4])
|
||||
self.assertEqual(subscribe.episode_priority, {"1": 90, "2": 90, "3": 90, "4": 90})
|
||||
self.assertNotIn("current_priority", updates[-1])
|
||||
self.assertEqual(subscribe.current_priority, 90)
|
||||
self.assertEqual(updates[-1]["current_priority"], 90)
|
||||
|
||||
def test_full_best_version_does_not_write_baseline_without_confirmed_full_coverage(self):
|
||||
subscribe = self._build_subscribe(
|
||||
best_version=1,
|
||||
best_version_full=1,
|
||||
current_priority=82,
|
||||
episode_priority={"1": 60},
|
||||
)
|
||||
|
||||
with patch.object(self.module, "SubscribeOper") as subscribe_oper_cls:
|
||||
self.SubscribeChain()._SubscribeChain__record_subscribe_download_facts(
|
||||
subscribe,
|
||||
mediainfo=SimpleNamespace(title_year="下载事实剧 (2026)"),
|
||||
downloads=[self._download(episodes=[1], pri_order=90, confirmed_full_coverage=False)],
|
||||
)
|
||||
|
||||
payload = subscribe_oper_cls.return_value.update.call_args.args[1]
|
||||
self.assertEqual(subscribe.current_priority, 82)
|
||||
self.assertNotIn("current_priority", payload)
|
||||
|
||||
def test_normal_subscription_without_episode_list_does_not_use_target_range_without_download_confirmation(self):
|
||||
subscribe = self._build_subscribe(best_version=0, best_version_full=0)
|
||||
|
||||
Reference in New Issue
Block a user