Files
MoviePilot/app/application/subscription/search.py

102 lines
3.2 KiB
Python

"""手工订阅搜索应用用例。"""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Optional
from app.application.subscription.contract import (
SubscribeDeletionCandidate,
SubscriptionStagingPort,
)
@dataclass(frozen=True, slots=True)
class SubscribeSearchActor:
"""执行手工订阅搜索的用户身份。"""
username: str
is_superuser: bool
@dataclass(frozen=True, slots=True)
class SubscriptionSearchSubmission:
"""一次手工搜索请求的入队结果。"""
batch_ids: tuple[str, ...]
target_count: int
queued_count: int
ongoing_count: int
single: bool
@property
def batch_id(self) -> Optional[str]:
"""返回最适合前端立即跟踪的批次编号。"""
return self.batch_ids[0] if self.batch_ids else None
SubscribeSearchSubmitter = Callable[
[tuple[int, ...], bool],
Awaitable[SubscriptionSearchSubmission],
]
class SearchSubscriptionsCommand:
"""按用户权限读取目标,并把手工搜索轻量提交到持久队列。"""
def __init__(
self,
repository: SubscriptionStagingPort,
submit_search: SubscribeSearchSubmitter,
) -> None:
"""注入订阅读取端口和持久队列提交端口。"""
self._repository = repository
self._submit_search = submit_search
async def execute(
self,
actor: SubscribeSearchActor,
subscribe_id: int | None = None,
mtype: Optional[str] = None,
) -> Optional[SubscriptionSearchSubmission]:
"""提交单条或指定媒体类型的可搜索订阅,目标不可访问时返回空。"""
if subscribe_id is not None:
candidate = await self._repository.get_candidate(subscribe_id)
if not self._can_access(candidate, actor) or (
mtype is not None
and (candidate is None or candidate.event_payload.get("type") != mtype)
):
return None
return await self._submit_search((subscribe_id,), True)
search_username = None if actor.is_superuser else actor.username
if mtype is None:
# 保留旧调用形态,兼容仍只实现全量查询参数的外部端口测试替身。
subscribe_ids = await self._repository.list_search_ids(search_username, "R")
else:
subscribe_ids = await self._repository.list_search_ids(
search_username,
"R",
mtype=mtype,
)
if not subscribe_ids:
return SubscriptionSearchSubmission(
batch_ids=(),
target_count=0,
queued_count=0,
ongoing_count=0,
single=False,
)
return await self._submit_search(tuple(subscribe_ids), False)
@staticmethod
def _can_access(
candidate: SubscribeDeletionCandidate | None,
actor: SubscribeSearchActor,
) -> bool:
"""沿用订阅读取接口的超级用户和归属用户权限语义。"""
if candidate is None:
return False
if actor.is_superuser:
return True
return bool(candidate.username) and candidate.username == actor.username