Files
MoviePilot/app/application/subscription/identity.py
T

123 lines
4.2 KiB
Python

"""按媒体身份批量删除订阅的应用用例。"""
from datetime import datetime, timezone
from typing import Any, Callable, Protocol
from app.application.outbox import AsyncOutboxTransaction, OutboxIntent
from app.application.subscription.delete import (
AsyncUnitOfWork,
SubscribeDeletedPublisher,
SubscribeDeletionActor,
SubscribeDeletionCandidate,
build_subscribe_deleted_payload,
)
from app.schemas.types import MediaSource
class SubscribeIdentityDeletionRepository(Protocol):
"""按媒体身份删除订阅所需的数据访问端口。"""
async def list_candidates_by_identity(
self,
media_source: MediaSource,
media_id: str,
season: int | None,
music_type: str | None,
) -> list[SubscribeDeletionCandidate]:
"""读取匹配媒体身份的去重订阅快照。"""
...
async def stage_delete(self, subscribe_id: int) -> None:
"""把指定订阅登记为待删除,但不自行提交事务。"""
...
SubscribeDeletionEventErrorHandler = Callable[[int, Exception], None]
class DeleteSubscriptionsByIdentityCommand:
"""按媒体身份删除当前用户可访问的全部订阅。"""
def __init__(
self,
repository: SubscribeIdentityDeletionRepository,
unit_of_work: AsyncUnitOfWork,
publish_deleted: SubscribeDeletedPublisher,
handle_event_error: SubscribeDeletionEventErrorHandler,
outbox: AsyncOutboxTransaction | None = None,
) -> None:
"""注入数据访问、事务、事件和事件错误处理端口。"""
self._repository = repository
self._unit_of_work = unit_of_work
self._publish_deleted = publish_deleted
self._handle_event_error = handle_event_error
self._outbox = outbox
async def execute(
self,
media_source: MediaSource,
media_id: str,
season: int | None,
music_type: str | None,
actor: SubscribeDeletionActor,
) -> int:
"""删除匹配订阅,并在提交后逐条发送兼容事件。"""
candidates = await self._repository.list_candidates_by_identity(
media_source,
media_id,
season,
music_type,
)
deletions = [
candidate
for candidate in candidates
if self._can_delete(candidate, actor)
]
events: list[tuple[SubscribeDeletionCandidate, dict[str, Any]]] = []
for candidate in deletions:
await self._repository.stage_delete(candidate.subscribe_id)
event_payload = build_subscribe_deleted_payload(
candidate.subscribe_id,
candidate.event_payload,
)
events.append((candidate, event_payload))
try:
if self._outbox:
now = datetime.now(timezone.utc)
for _, event_payload in events:
await self._outbox.stage(
OutboxIntent(
event_key=event_payload["idempotency_key"],
topic="subscribe.deleted",
payload=event_payload,
),
now,
)
await self._unit_of_work.commit()
except Exception:
await self._unit_of_work.rollback()
raise
for candidate, event_payload in events:
try:
await self._publish_deleted(event_payload)
if self._outbox:
await self._outbox.complete_by_event_key(
event_payload["idempotency_key"],
datetime.now(timezone.utc),
)
except Exception as error:
self._handle_event_error(candidate.subscribe_id, error)
return len(deletions)
@staticmethod
def _can_delete(
candidate: SubscribeDeletionCandidate,
actor: SubscribeDeletionActor,
) -> bool:
"""判断用户是否拥有候选订阅的删除权限。"""
if actor.is_superuser:
return True
return bool(candidate.username) and candidate.username == actor.username