mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 21:07:01 +08:00
fix(transfer): suppress completion events without target items
This commit is contained in:
@@ -345,7 +345,7 @@ class TransferSettlementOwner(_TransferOwnerBase):
|
||||
self, task: TransferTask, transferinfo: TransferInfo, /
|
||||
) -> Tuple[bool, str]:
|
||||
"""
|
||||
整理完成后处理
|
||||
原子结算整理结果;成功但无目标项的静默跳过不发布完成事件。
|
||||
"""
|
||||
# 状态
|
||||
ret_status = True
|
||||
@@ -504,7 +504,12 @@ class TransferSettlementOwner(_TransferOwnerBase):
|
||||
# 转移成功
|
||||
logger.info(f"{task.fileitem.name} 入库成功:{target_dir_path or ''}")
|
||||
|
||||
durable_event = self._durable_transfer_event(task, success=True)
|
||||
# 静默跳过沿用成功终态,但没有目标文件可供完成事件的消费者处理。
|
||||
durable_event = (
|
||||
self._durable_transfer_event(task, success=True)
|
||||
if transferinfo.target_item is not None
|
||||
else None
|
||||
)
|
||||
topic = durable_event[0] if durable_event else None
|
||||
event_type = durable_event[1] if durable_event else None
|
||||
event_payload = self._transfer_result_payload(task, transferinfo)
|
||||
|
||||
@@ -58,6 +58,10 @@ Event Contract Registry 是 53 个事件的逐项机器清单。下表按相同
|
||||
`AudioTransferComplete`、`AudioTransferFailed`:整理步骤本身属于 E3,
|
||||
但历史行提交后向事件消费者发布结果属于 E2,使用同一 JSON 快照与恢复 handler。
|
||||
|
||||
宿主仅在整理成功且 `transferinfo.target_item` 非空时登记媒体、字幕或音频的完成事件。
|
||||
静默跳过即使返回 `success=True`,也不产生完成事件的 outbox intent 或广播;任务终态与历史
|
||||
仍按原成功路径结算。失败结果继续登记对应失败事件,无需目标项。
|
||||
|
||||
## 非 Event 后台机制映射
|
||||
|
||||
### FastAPI BackgroundTasks
|
||||
|
||||
97
tests/test_transfer_completed_event.py
Normal file
97
tests/test_transfer_completed_event.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""整理完成事件只对应实际目标文件,静默跳过仍正常结算。"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.schemas.transfer import TransferInfo
|
||||
from app.schemas.types import EventType
|
||||
from tests.test_transfer_job_manager import (
|
||||
FakeMedia,
|
||||
bind_terminal_checkpoint,
|
||||
make_fileitem,
|
||||
make_task,
|
||||
make_transfer_chain,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("extension", "completed_topic", "completed_event", "failed_topic", "failed_event"),
|
||||
[
|
||||
("mkv", "transfer.completed", EventType.TransferComplete,
|
||||
"transfer.failed", EventType.TransferFailed),
|
||||
("srt", "transfer.subtitle.completed", EventType.SubtitleTransferComplete,
|
||||
"transfer.subtitle.failed", EventType.SubtitleTransferFailed),
|
||||
("flac", "transfer.audio.completed", EventType.AudioTransferComplete,
|
||||
"transfer.audio.failed", EventType.AudioTransferFailed),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("outcome", ["skipped", "without_target", "completed", "failed"])
|
||||
def test_transfer_result_events_require_completed_target(
|
||||
extension: str,
|
||||
completed_topic: str,
|
||||
completed_event: EventType,
|
||||
failed_topic: str,
|
||||
failed_event: EventType,
|
||||
outcome: str,
|
||||
) -> None:
|
||||
"""视频、字幕和音频跳过不发完成事件,正常完成和失败保留原事件与终态。"""
|
||||
chain = make_transfer_chain()
|
||||
chain.eventmanager = MagicMock()
|
||||
chain.queue_failed_transfer_notification = MagicMock()
|
||||
chain._TransferChain__mark_torrent_completed_if_done = MagicMock()
|
||||
chain._record_scrape_target = MagicMock()
|
||||
chain._send_metadata_scrape_event = MagicMock()
|
||||
task = make_task(1)
|
||||
task.fileitem = make_fileitem(f"/downloads/Test.Show.S01E01.{extension}")
|
||||
task.mediainfo = FakeMedia()
|
||||
assert chain._TransferChain__put_to_jobview(task)
|
||||
transferinfo = TransferInfo(
|
||||
success=outcome != "failed",
|
||||
message="综艺非正片已全部跳过(1 个文件)" if outcome == "skipped" else None,
|
||||
fileitem=task.fileitem,
|
||||
target_item=(
|
||||
make_fileitem(f"/library/Test.Show.S01E01.{extension}")
|
||||
if outcome == "completed"
|
||||
else None
|
||||
),
|
||||
transfer_type="copy",
|
||||
need_notify=False,
|
||||
)
|
||||
bind_terminal_checkpoint(task, transferinfo)
|
||||
with patch(
|
||||
"app.chain.transfer.settlement.add_transfer_success",
|
||||
return_value=SimpleNamespace(id=1),
|
||||
) as add_success, patch(
|
||||
"app.chain.transfer.settlement.add_transfer_fail",
|
||||
return_value=SimpleNamespace(id=1),
|
||||
) as add_fail, patch(
|
||||
"app.chain.transfer.settlement.clear_transfer_failures",
|
||||
), patch(
|
||||
"app.chain.transfer.settlement.record_transfer_failure",
|
||||
):
|
||||
state, _message = chain._TransferChain__default_callback(task, transferinfo)
|
||||
|
||||
assert state is (outcome != "failed")
|
||||
chain.durable_event_writer.transfer_result.assert_called_once()
|
||||
call = chain.durable_event_writer.transfer_result.call_args.kwargs
|
||||
assert call["settlement"].outcome == ("failed" if outcome == "failed" else "succeeded")
|
||||
assert task.terminal_settled is True
|
||||
if outcome == "failed":
|
||||
add_fail.assert_called_once()
|
||||
add_success.assert_not_called()
|
||||
assert call["topic"] == failed_topic
|
||||
assert chain.eventmanager.send_event.call_args.args[0] is failed_event
|
||||
else:
|
||||
add_success.assert_called_once()
|
||||
add_fail.assert_not_called()
|
||||
chain.queue_failed_transfer_notification.assert_not_called()
|
||||
if outcome in ("skipped", "without_target"):
|
||||
assert call["topic"] is None
|
||||
chain.eventmanager.send_event.assert_not_called()
|
||||
else:
|
||||
assert call["topic"] == completed_topic
|
||||
event_type, payload = chain.eventmanager.send_event.call_args.args
|
||||
assert event_type is completed_event
|
||||
assert payload["transferinfo"].target_item == transferinfo.target_item
|
||||
Reference in New Issue
Block a user