mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 03:18:14 +08:00
206 lines
7.9 KiB
Python
206 lines
7.9 KiB
Python
"""
|
|
手动下载后,订阅检查应把部分入库事实同步到卡片进度。
|
|
"""
|
|
|
|
from contextlib import contextmanager
|
|
from dataclasses import replace
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
import app.chain.subscribe.refresh as subscribe_refresh
|
|
from app.application.subscription.contract import SubscriptionSnapshot, build_subscribe_meta
|
|
from app.application.subscription.mutation import SyncSubscriptionMutationService
|
|
from app.chain.download import DownloadChain
|
|
from app.chain.subscribe.facade import SubscribeChain
|
|
from app.db.adapters.outbox import SqlAlchemyOutboxDispatchStore, SqlAlchemyOutboxStager
|
|
from app.db.adapters.subscription import SessionSubscriptionRepository
|
|
from app.db.models.outbox import OutboxMessage
|
|
from app.db.models.subscribe import Subscribe
|
|
from app.db.session import SessionFactory
|
|
from app.db.uow import SqlAlchemyUnitOfWork
|
|
from app.domain.context import MediaInfo
|
|
from app.schemas.mediaserver import ExistMediaInfo
|
|
from app.schemas.types import MediaSource, MediaType
|
|
|
|
|
|
@pytest.fixture
|
|
def progress_scope(monkeypatch):
|
|
"""
|
|
保留缺集计算和进度派生,仅隔离媒体库与持久化边界。
|
|
"""
|
|
subscribe = SubscriptionSnapshot(
|
|
id=6966, name="Doctor X", type=MediaType.TV.value,
|
|
media_source=MediaSource.TMDB, media_id="6966", season=1,
|
|
total_episode=12, lack_episode=11, note=[1], state="R",
|
|
last_update="2026-10-01 12:00:00",
|
|
)
|
|
media = MediaInfo(
|
|
title="Doctor X", type=MediaType.TV,
|
|
media_source=MediaSource.TMDB, media_id="6966",
|
|
seasons={1: list(range(1, 13))},
|
|
)
|
|
download = object.__new__(DownloadChain)
|
|
download._media_exists_with_server_cache = Mock(
|
|
return_value=ExistMediaInfo(seasons={1: [1, 2]})
|
|
)
|
|
monkeypatch.setattr(subscribe_refresh, "DownloadChain", lambda: download)
|
|
chain = object.__new__(SubscribeChain)
|
|
chain.finish_subscribe_or_not = Mock()
|
|
chain._SubscribeChain__apply_episodes_refresh = Mock(return_value=12)
|
|
state = SimpleNamespace(subscribe=subscribe, media=media, chain=chain, download=download)
|
|
|
|
def apply_update(current, fields, **_kwargs):
|
|
"""
|
|
保存派生快照,供重复检查读取同一持久化状态。
|
|
"""
|
|
state.subscribe = replace(current, **fields)
|
|
return state.subscribe
|
|
|
|
chain._SubscribeChain__apply_subscribe_update = Mock(side_effect=apply_update)
|
|
return state
|
|
|
|
|
|
def _check(scope):
|
|
"""
|
|
使用当前订阅快照执行真实的现存媒体检查。
|
|
"""
|
|
return scope.chain.check_and_handle_existing_media(
|
|
subscribe=scope.subscribe, meta=build_subscribe_meta(scope.subscribe),
|
|
mediainfo=scope.media, mediakey="tmdb:6966",
|
|
)
|
|
|
|
|
|
def test_partial_library_updates_stale_card_progress(progress_scope):
|
|
"""
|
|
前两集已入库且 note 只含第一集时,卡片应从 1/12 校正为 2/12。
|
|
"""
|
|
for _ in range(2):
|
|
completed, missing = _check(progress_scope)
|
|
assert not completed
|
|
assert missing["tmdb:6966"][1].episodes == list(range(3, 13))
|
|
assert progress_scope.subscribe.lack_episode == 10
|
|
assert progress_scope.subscribe.note == [1]
|
|
assert progress_scope.subscribe.last_update == "2026-10-01 12:00:00"
|
|
progress_scope.chain.finish_subscribe_or_not.assert_not_called()
|
|
|
|
|
|
def test_partial_library_respects_start_and_downloaded_facts(progress_scope):
|
|
"""
|
|
进度复用订阅范围和已下载事实,不重复计数或包含其他季。
|
|
"""
|
|
progress_scope.subscribe = replace(progress_scope.subscribe, start_episode=3, note=[3, 5])
|
|
progress_scope.download._media_exists_with_server_cache.return_value = ExistMediaInfo(
|
|
seasons={1: [1, 2, 3, 4, 4], 2: list(range(1, 13))}
|
|
)
|
|
|
|
completed, missing = _check(progress_scope)
|
|
|
|
assert not completed
|
|
assert missing["tmdb:6966"][1].episodes == list(range(6, 13))
|
|
assert progress_scope.subscribe.lack_episode == 7
|
|
progress_scope.chain.finish_subscribe_or_not.assert_not_called()
|
|
|
|
|
|
def test_failed_missing_query_preserves_progress(progress_scope):
|
|
"""
|
|
无法取得缺集事实时,不能把未知状态当成全部完成。
|
|
"""
|
|
progress_scope.chain.resolve_subscribe_missing = Mock(return_value=(False, {}))
|
|
|
|
completed, missing = _check(progress_scope)
|
|
|
|
assert not completed
|
|
assert missing == {}
|
|
assert progress_scope.subscribe.lack_episode == 11
|
|
progress_scope.chain._SubscribeChain__apply_subscribe_update.assert_not_called()
|
|
progress_scope.chain.finish_subscribe_or_not.assert_not_called()
|
|
|
|
|
|
def test_best_version_check_preserves_quality_progress(progress_scope):
|
|
"""
|
|
洗版订阅继续按下载质量事实处理,不用部分入库覆盖其进度。
|
|
"""
|
|
progress_scope.subscribe = replace(
|
|
progress_scope.subscribe, best_version=1, episode_priority={"1": 80},
|
|
)
|
|
|
|
completed, missing = _check(progress_scope)
|
|
|
|
assert not completed
|
|
assert missing
|
|
assert progress_scope.subscribe.lack_episode == 11
|
|
assert progress_scope.subscribe.episode_priority == {"1": 80}
|
|
progress_scope.chain._SubscribeChain__apply_subscribe_update.assert_not_called()
|
|
progress_scope.chain.finish_subscribe_or_not.assert_not_called()
|
|
|
|
|
|
def test_complete_library_keeps_completion_path(progress_scope):
|
|
"""
|
|
整季入库仍交给原有完成流程,避免在检查入口重复结算。
|
|
"""
|
|
progress_scope.download._media_exists_with_server_cache.return_value = ExistMediaInfo(
|
|
seasons={1: list(range(1, 13))}
|
|
)
|
|
|
|
completed, missing = _check(progress_scope)
|
|
|
|
assert completed
|
|
assert missing == {}
|
|
progress_scope.chain.finish_subscribe_or_not.assert_called_once()
|
|
assert progress_scope.chain.finish_subscribe_or_not.call_args.kwargs["force"]
|
|
progress_scope.chain._SubscribeChain__apply_subscribe_update.assert_not_called()
|
|
|
|
|
|
def test_partial_progress_persists_without_overwriting_extra_fields(progress_scope, db):
|
|
"""
|
|
真实订阅写入只更新缺失集数,保留数据库中较新的附加字段。
|
|
"""
|
|
db.watermark(Subscribe, OutboxMessage)
|
|
row = db.add(Subscribe(
|
|
name="Doctor X", type=MediaType.TV.value,
|
|
media_source=MediaSource.TMDB.value, media_id="6966", season=1,
|
|
total_episode=12, lack_episode=11, note=[1, 99],
|
|
episode_priority={"1": 80, "99": 90}, current_priority=80,
|
|
sites=[1, 2], filter_groups=["WEB-DL"], state="R", best_version=0,
|
|
last_update="2026-10-01 12:00:00", manual_total_episode=1,
|
|
))
|
|
progress_scope.subscribe = replace(progress_scope.subscribe, id=row.id, manual_total_episode=1)
|
|
published = []
|
|
|
|
@contextmanager
|
|
def mutation_scope():
|
|
"""
|
|
用真实短事务和 outbox 保存进度并捕获修改事件。
|
|
"""
|
|
with SessionFactory() as session:
|
|
yield SyncSubscriptionMutationService(
|
|
repository=SessionSubscriptionRepository(session),
|
|
unit_of_work=SqlAlchemyUnitOfWork(session),
|
|
outbox=SqlAlchemyOutboxStager(session),
|
|
dispatch_store=SqlAlchemyOutboxDispatchStore(SessionFactory),
|
|
publish_modified=published.append,
|
|
)
|
|
|
|
progress_scope.chain.sync_subscription_mutation_scope = mutation_scope
|
|
progress_scope.chain._SubscribeChain__apply_subscribe_update = (
|
|
SubscribeChain._SubscribeChain__apply_subscribe_update.__get__(progress_scope.chain)
|
|
)
|
|
|
|
completed, _missing = _check(progress_scope)
|
|
|
|
assert not completed
|
|
db.session.expire_all()
|
|
stored = db.session.get(Subscribe, row.id)
|
|
assert stored.lack_episode == 10
|
|
assert stored.note == [1, 99]
|
|
assert stored.episode_priority == {"1": 80, "99": 90}
|
|
assert stored.current_priority == 80
|
|
assert stored.sites == [1, 2]
|
|
assert stored.filter_groups == ["WEB-DL"]
|
|
assert stored.last_update == "2026-10-01 12:00:00"
|
|
assert len(published) == 1
|
|
assert published[0]["subscribe_info"]["lack_episode"] == 10
|
|
assert published[0]["subscribe_info"]["note"] == [1, 99]
|