mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
fix(subscribe): persist best-version downloads to note and read it back (#5783)
This commit is contained in:
+11
-4
@@ -1189,11 +1189,14 @@ class SubscribeChain(ChainBase):
|
|||||||
mediakey = subscribe.tmdbid or subscribe.doubanid
|
mediakey = subscribe.tmdbid or subscribe.doubanid
|
||||||
# 是否有剩余集
|
# 是否有剩余集
|
||||||
no_lefts = not lefts or not lefts.get(mediakey)
|
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:
|
if not subscribe.best_version:
|
||||||
# 订阅存在待定策略,不管是否已完成,均需更新订阅信息
|
|
||||||
# 更新订阅已下载信息
|
|
||||||
self.__update_subscribe_note(subscribe=subscribe, downloads=downloads)
|
|
||||||
# 更新订阅剩余集数和时间
|
# 更新订阅剩余集数和时间
|
||||||
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, mediainfo=mediainfo,
|
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, mediainfo=mediainfo,
|
||||||
update_date=bool(downloads))
|
update_date=bool(downloads))
|
||||||
@@ -1856,7 +1859,11 @@ class SubscribeChain(ChainBase):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_downloaded(subscribe: Subscribe) -> List[int]:
|
def __get_downloaded(subscribe: Subscribe) -> List[int]:
|
||||||
"""
|
"""
|
||||||
获取已下载过的集数或电影
|
获取已下载过的集数或电影。
|
||||||
|
|
||||||
|
洗版分支只返回 priority==100 的完成集;priority<100 的集仍要继续搜索更高
|
||||||
|
优先级版本,不能并入返回值(会让下游把 pending 减空、订阅卡死)。
|
||||||
|
note 由非洗版分支消费,用于洗版关闭后的迁移读取。
|
||||||
"""
|
"""
|
||||||
if subscribe.best_version:
|
if subscribe.best_version:
|
||||||
if subscribe.type == MediaType.TV.value:
|
if subscribe.type == MediaType.TV.value:
|
||||||
|
|||||||
@@ -925,3 +925,151 @@ class SubscribeFilterAllowedEpisodesTest(TestCase):
|
|||||||
self.assertEqual(_context.allowed_episodes, set(range(84, 93)))
|
self.assertEqual(_context.allowed_episodes, set(range(84, 93)))
|
||||||
# 浅拷贝 + 新字段写入不应反向污染源 context(match() 中 contexts 缓存可能跨多次匹配复用)。
|
# 浅拷贝 + 新字段写入不应反向污染源 context(match() 中 contexts 缓存可能跨多次匹配复用)。
|
||||||
self.assertIsNone(original_context.allowed_episodes)
|
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