mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-01 13:40:54 +08:00
fix(subscribe): persist best-version downloads to note and read it back (#5783)
This commit is contained in:
@@ -1189,11 +1189,14 @@ class SubscribeChain(ChainBase):
|
||||
mediakey = subscribe.tmdbid or subscribe.doubanid
|
||||
# 是否有剩余集
|
||||
no_lefts = not lefts or not lefts.get(mediakey)
|
||||
# 不论是否洗版,只要本轮有下载产生就要把集数追加进 subscribe.note,
|
||||
# 保证"已下载过哪些集"这条事实在所有订阅模式下都有可靠落点;洗版分支
|
||||
# 之前只写 episode_priority,导致用户切回普通订阅时丢失下载历史,并让
|
||||
# __get_downloaded 在洗版下无法从 note 拿到 priority 未达 100 但实际下过的集。
|
||||
if downloads:
|
||||
self.__update_subscribe_note(subscribe=subscribe, downloads=downloads)
|
||||
# 是否完成订阅
|
||||
if not subscribe.best_version:
|
||||
# 订阅存在待定策略,不管是否已完成,均需更新订阅信息
|
||||
# 更新订阅已下载信息
|
||||
self.__update_subscribe_note(subscribe=subscribe, downloads=downloads)
|
||||
# 更新订阅剩余集数和时间
|
||||
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, mediainfo=mediainfo,
|
||||
update_date=bool(downloads))
|
||||
@@ -1856,7 +1859,11 @@ class SubscribeChain(ChainBase):
|
||||
@staticmethod
|
||||
def __get_downloaded(subscribe: Subscribe) -> List[int]:
|
||||
"""
|
||||
获取已下载过的集数或电影
|
||||
获取已下载过的集数或电影。
|
||||
|
||||
洗版分支只返回 priority==100 的完成集;priority<100 的集仍要继续搜索更高
|
||||
优先级版本,不能并入返回值(会让下游把 pending 减空、订阅卡死)。
|
||||
note 由非洗版分支消费,用于洗版关闭后的迁移读取。
|
||||
"""
|
||||
if subscribe.best_version:
|
||||
if subscribe.type == MediaType.TV.value:
|
||||
|
||||
@@ -925,3 +925,151 @@ class SubscribeFilterAllowedEpisodesTest(TestCase):
|
||||
self.assertEqual(_context.allowed_episodes, set(range(84, 93)))
|
||||
# 浅拷贝 + 新字段写入不应反向污染源 context(match() 中 contexts 缓存可能跨多次匹配复用)。
|
||||
self.assertIsNone(original_context.allowed_episodes)
|
||||
|
||||
|
||||
class SubscribeNoteTrackingTest(TestCase):
|
||||
"""覆盖洗版与非洗版下 subscribe.note 的下载历史追踪。
|
||||
|
||||
回归目标:finish_subscribe_or_not 必须在所有订阅模式下都把本轮下载的集数追加进
|
||||
subscribe.note;__get_downloaded 在洗版分支必须把 note 与 episode_priority==100
|
||||
的完成集合并返回,避免迁移或低优先级下载场景下已下集被误判为"未下载"。
|
||||
"""
|
||||
|
||||
def _build_subscribe(self, **overrides):
|
||||
return SubscribeChainTest()._build_subscribe(**overrides)
|
||||
|
||||
@staticmethod
|
||||
def _build_download_context(episodes):
|
||||
"""构造一个最小化下载 context:只携带 finish_subscribe_or_not / __update_subscribe_note 路径会读到的字段。"""
|
||||
return SimpleNamespace(
|
||||
meta_info=SimpleNamespace(season_list=[1], episode_list=list(episodes)),
|
||||
media_info=SimpleNamespace(
|
||||
type=MediaType.TV,
|
||||
tmdb_id=1,
|
||||
douban_id=None,
|
||||
),
|
||||
torrent_info=SimpleNamespace(pri_order=99, title="fake-torrent"),
|
||||
selected_episodes=list(episodes),
|
||||
)
|
||||
|
||||
def test_finish_subscribe_writes_note_for_best_version_downloads(self):
|
||||
"""洗版分支若产生 downloads,subscribe.note 必须被追加,不再被 best_version 标志拦截。
|
||||
|
||||
旧逻辑只在非洗版分支调用 __update_subscribe_note,导致 best_version=1 时
|
||||
下载历史只落在 episode_priority;用户切回普通订阅或排障对账时缺失"下过哪些集"
|
||||
的事实源。这条用例验证修复后两个分支都会写 note。
|
||||
"""
|
||||
subscribe = self._build_subscribe(
|
||||
best_version=1,
|
||||
total_episode=92,
|
||||
episode_priority={"1": 100},
|
||||
note=[1],
|
||||
)
|
||||
chain = SubscribeChain()
|
||||
downloads = [self._build_download_context([83])]
|
||||
|
||||
captured_updates = []
|
||||
|
||||
class _SubscribeOper:
|
||||
def update(self, subscribe_id, payload):
|
||||
captured_updates.append((subscribe_id, payload))
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return subscribe
|
||||
|
||||
with patch.object(SUBSCRIBE_CHAIN_MODULE, "SubscribeOper", _SubscribeOper), patch.object(
|
||||
SubscribeChain,
|
||||
"update_subscribe_priority",
|
||||
), patch.object(
|
||||
SubscribeChain,
|
||||
"_SubscribeChain__finish_subscribe",
|
||||
):
|
||||
chain.finish_subscribe_or_not(
|
||||
subscribe=subscribe,
|
||||
meta=SimpleNamespace(type=MediaType.TV),
|
||||
mediainfo=SimpleNamespace(title_year="Test Show (2026)", type=MediaType.TV,
|
||||
tmdb_id=1, douban_id=None),
|
||||
downloads=downloads,
|
||||
lefts=None,
|
||||
)
|
||||
|
||||
# note 更新必然发生在 SubscribeOper.update 上,定位"note" 键的最近一次写入。
|
||||
note_writes = [payload["note"] for _, payload in captured_updates if "note" in payload]
|
||||
self.assertTrue(note_writes, "best_version downloads should still trigger note update")
|
||||
self.assertIn(83, note_writes[-1])
|
||||
self.assertIn(1, note_writes[-1]) # 既有 note 保留
|
||||
|
||||
def test_finish_subscribe_skips_note_when_no_downloads(self):
|
||||
"""没有 downloads 时不应触碰 note,避免空写入或误清除。"""
|
||||
subscribe = self._build_subscribe(best_version=1, total_episode=92, note=[1, 2])
|
||||
chain = SubscribeChain()
|
||||
|
||||
captured_updates = []
|
||||
|
||||
class _SubscribeOper:
|
||||
def update(self, subscribe_id, payload):
|
||||
captured_updates.append((subscribe_id, payload))
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return subscribe
|
||||
|
||||
with patch.object(SUBSCRIBE_CHAIN_MODULE, "SubscribeOper", _SubscribeOper), patch.object(
|
||||
SubscribeChain,
|
||||
"_SubscribeChain__is_best_version_complete",
|
||||
return_value=False,
|
||||
), patch.object(
|
||||
SubscribeChain,
|
||||
"_SubscribeChain__finish_subscribe",
|
||||
):
|
||||
chain.finish_subscribe_or_not(
|
||||
subscribe=subscribe,
|
||||
meta=SimpleNamespace(type=MediaType.TV),
|
||||
mediainfo=SimpleNamespace(title_year="Test Show (2026)", type=MediaType.TV,
|
||||
tmdb_id=1, douban_id=None),
|
||||
downloads=None,
|
||||
lefts=None,
|
||||
)
|
||||
|
||||
# 无下载时不应该有 note 写入。
|
||||
self.assertFalse(
|
||||
[payload for _, payload in captured_updates if "note" in payload],
|
||||
"note must not be touched when downloads is empty",
|
||||
)
|
||||
|
||||
def test_get_downloaded_best_version_returns_only_completed_episodes(self):
|
||||
"""关键回归:洗版分支不得把 note 合并进 __get_downloaded 返回值。
|
||||
|
||||
否则 check_and_handle_existing_media → __get_subscribe_no_exits 会把
|
||||
priority<100 但已下载的集从 pending no_exists 中减掉,配合 force=True 但
|
||||
__is_best_version_complete=False 的 finish_subscribe_or_not,会让订阅每轮
|
||||
都跳过搜索却又永远不完成。__get_downloaded 在洗版下的语义是"无需再处理的
|
||||
集",只有 priority==100 才满足该语义。
|
||||
"""
|
||||
subscribe = self._build_subscribe(
|
||||
best_version=1,
|
||||
total_episode=3,
|
||||
episode_priority={"1": 100, "2": 100, "3": 99},
|
||||
note=[1, 2, 3],
|
||||
)
|
||||
|
||||
downloaded = SubscribeChain._SubscribeChain__get_downloaded(subscribe)
|
||||
|
||||
# E3 priority=99 仍是 pending,绝对不能合并到 downloaded 里
|
||||
self.assertEqual(downloaded, [1, 2])
|
||||
self.assertNotIn(3, downloaded)
|
||||
|
||||
def test_get_downloaded_non_best_version_reads_note_after_wash_migration(self):
|
||||
"""迁移场景:洗版期间 finish_subscribe_or_not 把下载集写入 note;
|
||||
用户随后把 best_version 关掉,订阅切回普通模式时 __get_downloaded
|
||||
从非洗版分支读取 note,旧洗版集仍能作为"已下载"被识别,避免重新匹配。
|
||||
"""
|
||||
subscribe = self._build_subscribe(
|
||||
best_version=0,
|
||||
total_episode=5,
|
||||
episode_priority={"1": 100, "2": 99}, # 旧洗版残留,普通分支不读
|
||||
note=[1, 2, 3],
|
||||
)
|
||||
|
||||
downloaded = SubscribeChain._SubscribeChain__get_downloaded(subscribe)
|
||||
|
||||
self.assertEqual(downloaded, [1, 2, 3])
|
||||
|
||||
Reference in New Issue
Block a user