mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 20:17:13 +08:00
169 lines
6.1 KiB
Python
169 lines
6.1 KiB
Python
"""订阅完成应用命令及其同步事务端口。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Mapping
|
|
from contextlib import AbstractContextManager
|
|
from datetime import datetime, timezone
|
|
from typing import Any, Protocol
|
|
|
|
from app.application.outbox import OutboxIntent, SyncOutboxTransaction, SyncUnitOfWork
|
|
|
|
|
|
class SubscriptionCompletionRepository(Protocol):
|
|
"""订阅完成命令需要的最小同步持久化端口。"""
|
|
|
|
def add_history(self, **payload: Any) -> None:
|
|
"""在当前事务中暂存订阅历史。"""
|
|
...
|
|
|
|
def delete(self, subscribe_id: int) -> None:
|
|
"""在当前事务中暂存订阅删除。"""
|
|
...
|
|
|
|
|
|
CompletionEffect = Callable[[], None]
|
|
CompletionReporter = Callable[[Mapping[str, Any]], object]
|
|
|
|
|
|
class CompleteSubscriptionCommand:
|
|
"""原子完成订阅,并按通知、事件、统计顺序执行提交后副作用。"""
|
|
|
|
def __init__(
|
|
self,
|
|
repository: SubscriptionCompletionRepository,
|
|
unit_of_work: SyncUnitOfWork,
|
|
outbox: SyncOutboxTransaction | None,
|
|
publish: Callable[[dict[str, Any]], None],
|
|
) -> None:
|
|
"""注入共享同步会话、事件发布端口和可选 durable outbox。"""
|
|
self._repository = repository
|
|
self._unit_of_work = unit_of_work
|
|
self._outbox = outbox
|
|
self._publish = publish
|
|
|
|
def execute(
|
|
self,
|
|
subscribe_id: int,
|
|
subscribe_info: Mapping[str, Any],
|
|
mediainfo: Mapping[str, Any],
|
|
notify: CompletionEffect,
|
|
report: CompletionReporter,
|
|
notification: Mapping[str, Any] | None = None,
|
|
) -> None:
|
|
"""在同一事务中写历史、删订阅并暂存完成事件、通知与统计意图。"""
|
|
info = dict(subscribe_info)
|
|
event_payload = {
|
|
"subscribe_id": subscribe_id,
|
|
"subscribe_info": info,
|
|
"mediainfo": dict(mediainfo),
|
|
"idempotency_key": completion_event_key(subscribe_id, info),
|
|
}
|
|
event_key = event_payload["idempotency_key"]
|
|
report_key = completion_report_key(subscribe_id, info)
|
|
notification_key = completion_notification_key(subscribe_id, info)
|
|
report_payload = {"subscribe_info": _completion_report_payload(info, report_key)}
|
|
try:
|
|
self._repository.add_history(**info)
|
|
self._repository.delete(subscribe_id)
|
|
if self._outbox:
|
|
now = datetime.now(timezone.utc)
|
|
self._outbox.stage(
|
|
OutboxIntent(
|
|
event_key=event_key,
|
|
topic="subscribe.complete",
|
|
payload=event_payload,
|
|
),
|
|
now,
|
|
)
|
|
if notification:
|
|
self._outbox.stage(
|
|
OutboxIntent(
|
|
event_key=notification_key,
|
|
topic="subscribe.complete.notification",
|
|
payload={
|
|
"idempotency_key": notification_key,
|
|
"message": dict(notification),
|
|
},
|
|
),
|
|
now,
|
|
)
|
|
self._outbox.stage(
|
|
OutboxIntent(
|
|
event_key=report_key,
|
|
topic="subscribe.complete.report",
|
|
payload=report_payload,
|
|
),
|
|
now,
|
|
)
|
|
self._unit_of_work.commit()
|
|
except Exception:
|
|
self._unit_of_work.rollback()
|
|
raise
|
|
|
|
notify()
|
|
if self._outbox and notification:
|
|
self._outbox.complete_by_event_key(
|
|
notification_key,
|
|
datetime.now(timezone.utc),
|
|
)
|
|
self._publish(event_payload)
|
|
if self._outbox:
|
|
self._outbox.complete_by_event_key(event_key, datetime.now(timezone.utc))
|
|
if report(report_payload["subscribe_info"]) is False:
|
|
raise RuntimeError("订阅完成统计上报未确认")
|
|
if self._outbox:
|
|
self._outbox.complete_by_event_key(report_key, datetime.now(timezone.utc))
|
|
|
|
|
|
def completion_event_key(subscribe_id: int, subscribe_info: Mapping[str, Any]) -> str:
|
|
"""构造跨重试稳定的订阅完成事件幂等键。"""
|
|
return (
|
|
f"subscribe.complete:{subscribe_id}:"
|
|
f"{subscribe_info.get('media_source') or 'unknown'}:"
|
|
f"{subscribe_info.get('media_id') or 'unknown'}:v1"
|
|
)
|
|
|
|
|
|
def completion_report_key(subscribe_id: int, subscribe_info: Mapping[str, Any]) -> str:
|
|
"""构造可独立重试的订阅完成统计幂等键。"""
|
|
return f"{completion_event_key(subscribe_id, subscribe_info)}:report"
|
|
|
|
|
|
def completion_notification_key(
|
|
subscribe_id: int,
|
|
subscribe_info: Mapping[str, Any],
|
|
) -> str:
|
|
"""构造订阅完成通知的稳定幂等键,避免恢复时重复生成不同消息。"""
|
|
return f"{completion_event_key(subscribe_id, subscribe_info)}:notification"
|
|
|
|
|
|
def _completion_report_payload(
|
|
subscribe_info: Mapping[str, Any],
|
|
report_key: str,
|
|
) -> dict[str, Any]:
|
|
"""保留旧统计接口字段,同时为恢复 handler 固化幂等键。"""
|
|
return {
|
|
"media_source": subscribe_info.get("media_source"),
|
|
"media_id": subscribe_info.get("media_id"),
|
|
"season": subscribe_info.get("season"),
|
|
"idempotency_key": report_key,
|
|
}
|
|
|
|
|
|
CompletionScope = Callable[[], AbstractContextManager[CompleteSubscriptionCommand]]
|
|
_configured_completion_scope: CompletionScope | None = None
|
|
|
|
|
|
def configure_subscription_completion_scope(provider: CompletionScope) -> None:
|
|
"""由启动组合根登记订阅完成独占事务作用域。"""
|
|
global _configured_completion_scope
|
|
_configured_completion_scope = provider
|
|
|
|
|
|
def get_subscription_completion_scope() -> AbstractContextManager[CompleteSubscriptionCommand]:
|
|
"""返回一次独占同步订阅完成事务作用域。"""
|
|
if _configured_completion_scope is None:
|
|
raise RuntimeError("订阅完成事务作用域尚未配置")
|
|
return _configured_completion_scope()
|