mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-31 13:07:56 +08:00
351 lines
12 KiB
Python
351 lines
12 KiB
Python
"""插件只读数据查询的应用服务与领域端口。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
from dataclasses import dataclass
|
|
from functools import partial
|
|
from typing import Any, Callable, Generic, Protocol, TypeVar, cast
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from app.application.database import AsyncDatabaseExecutor
|
|
from app.schemas.history import DownloadHistory, TransferHistory
|
|
from app.schemas.query import (
|
|
DownloadHistoryFilter,
|
|
QueryPage,
|
|
QueryPageRequest,
|
|
SubscribeHistory,
|
|
SubscriptionFilter,
|
|
SubscriptionHistoryFilter,
|
|
TransferHistoryFilter,
|
|
)
|
|
from app.schemas.subscribe import Subscribe
|
|
|
|
RecordT = TypeVar("RecordT")
|
|
DtoT = TypeVar("DtoT", bound=BaseModel)
|
|
ResultT = TypeVar("ResultT")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class QueryRows(Generic[RecordT]):
|
|
"""查询端口返回的短生命周期结果,不跨越应用服务边界。"""
|
|
|
|
items: Sequence[RecordT]
|
|
total: int
|
|
|
|
|
|
class SubscriptionQueryPort(Protocol):
|
|
"""订阅领域查询端口,包含订阅和订阅完成历史两个只读切片。"""
|
|
|
|
def list_subscriptions(
|
|
self,
|
|
*,
|
|
filters: SubscriptionFilter,
|
|
page: QueryPageRequest,
|
|
) -> QueryRows[object]:
|
|
"""按组合筛选和稳定分页读取订阅记录。"""
|
|
...
|
|
|
|
def get_subscription(self, subscription_id: int) -> object | None:
|
|
"""按 ID 读取一条订阅记录。"""
|
|
...
|
|
|
|
def list_subscription_history(
|
|
self,
|
|
*,
|
|
filters: SubscriptionHistoryFilter,
|
|
page: QueryPageRequest,
|
|
) -> QueryRows[object]:
|
|
"""按组合筛选和稳定分页读取订阅完成历史。"""
|
|
...
|
|
|
|
def get_subscription_history(self, history_id: int) -> object | None:
|
|
"""按 ID 读取一条订阅完成历史。"""
|
|
...
|
|
|
|
|
|
class HistoryQueryPort(Protocol):
|
|
"""下载与整理历史查询端口。"""
|
|
|
|
def list_download_history(
|
|
self,
|
|
*,
|
|
filters: DownloadHistoryFilter,
|
|
page: QueryPageRequest,
|
|
) -> QueryRows[object]:
|
|
"""按组合筛选和稳定分页读取下载历史。"""
|
|
...
|
|
|
|
def get_download_history(self, history_id: int) -> object | None:
|
|
"""按 ID 读取一条下载历史。"""
|
|
...
|
|
|
|
def list_transfer_history(
|
|
self,
|
|
*,
|
|
filters: TransferHistoryFilter,
|
|
page: QueryPageRequest,
|
|
) -> QueryRows[object]:
|
|
"""按组合筛选和稳定分页读取整理历史。"""
|
|
...
|
|
|
|
def get_transfer_history(self, history_id: int) -> object | None:
|
|
"""按 ID 读取一条整理历史。"""
|
|
...
|
|
|
|
|
|
class DataQueryService:
|
|
"""把订阅和历史查询统一投影为不含持久化实现的 DTO。"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
subscriptions: SubscriptionQueryPort,
|
|
histories: HistoryQueryPort,
|
|
async_executor: AsyncDatabaseExecutor,
|
|
) -> None:
|
|
"""保存两个领域查询端口和异步数据库执行边界。"""
|
|
self._subscriptions = subscriptions
|
|
self._histories = histories
|
|
self._async_executor = async_executor
|
|
|
|
@staticmethod
|
|
def _page_request(page: QueryPageRequest | dict[str, Any] | None) -> QueryPageRequest:
|
|
"""把调用方输入规范化为有界分页合同。"""
|
|
if page is None:
|
|
return QueryPageRequest()
|
|
if isinstance(page, QueryPageRequest):
|
|
return page
|
|
return cast(QueryPageRequest, QueryPageRequest.model_validate(page))
|
|
|
|
@staticmethod
|
|
def _filter(
|
|
value: Any,
|
|
filter_type: type[SubscriptionFilter]
|
|
| type[SubscriptionHistoryFilter]
|
|
| type[DownloadHistoryFilter]
|
|
| type[TransferHistoryFilter],
|
|
) -> Any:
|
|
"""把字典筛选条件转换为带媒体身份校验的模型。"""
|
|
if value is None:
|
|
return filter_type()
|
|
if isinstance(value, filter_type):
|
|
return value
|
|
return filter_type.model_validate(value)
|
|
|
|
@staticmethod
|
|
def _rows(value: QueryRows[object]) -> QueryRows[object]:
|
|
"""校验端口结果的总数,避免负数污染分页合同。"""
|
|
if not isinstance(value, QueryRows):
|
|
raise TypeError("查询端口必须返回 QueryRows")
|
|
return QueryRows(
|
|
items=value.items,
|
|
total=max(int(value.total), 0),
|
|
)
|
|
|
|
@classmethod
|
|
def _to_page(
|
|
cls,
|
|
dto_type: type[DtoT],
|
|
page: QueryPageRequest,
|
|
rows: QueryRows[object],
|
|
) -> QueryPage[DtoT]:
|
|
"""在查询端口结果离开应用层前完成 DTO 投影。"""
|
|
normalized_rows = cls._rows(rows)
|
|
return cast(
|
|
QueryPage[DtoT],
|
|
QueryPage(
|
|
items=[dto_type.model_validate(item) for item in normalized_rows.items],
|
|
total=normalized_rows.total,
|
|
page=page.page,
|
|
count=page.count,
|
|
),
|
|
)
|
|
|
|
@staticmethod
|
|
def _to_item(dto_type: type[DtoT], item: object | None) -> DtoT | None:
|
|
"""把单条端口记录投影为稳定 DTO。"""
|
|
return dto_type.model_validate(item) if item is not None else None
|
|
|
|
async def _async_run(self, operation: Callable[[], ResultT]) -> ResultT:
|
|
"""通过统一数据库执行器运行一个同步查询用例。"""
|
|
return cast(ResultT, await self._async_executor.run(operation))
|
|
|
|
def list_subscriptions(
|
|
self,
|
|
filters: SubscriptionFilter | dict[str, Any] | None = None,
|
|
page: QueryPageRequest | dict[str, Any] | None = None,
|
|
) -> QueryPage[Subscribe]:
|
|
"""同步分页查询订阅。"""
|
|
normalized_page = self._page_request(page)
|
|
normalized_filters = self._filter(filters, SubscriptionFilter)
|
|
rows = self._subscriptions.list_subscriptions(
|
|
filters=normalized_filters,
|
|
page=normalized_page,
|
|
)
|
|
return self._to_page(Subscribe, normalized_page, rows)
|
|
|
|
async def async_list_subscriptions(
|
|
self,
|
|
filters: SubscriptionFilter | dict[str, Any] | None = None,
|
|
page: QueryPageRequest | dict[str, Any] | None = None,
|
|
) -> QueryPage[Subscribe]:
|
|
"""异步分页查询订阅,业务规则在数据库 worker 中复用同步入口。"""
|
|
return await self._async_run(
|
|
partial(self.list_subscriptions, filters, page)
|
|
)
|
|
|
|
def get_subscription(self, subscription_id: int) -> Subscribe | None:
|
|
"""同步按 ID 查询订阅。"""
|
|
return self._to_item(
|
|
Subscribe,
|
|
self._subscriptions.get_subscription(subscription_id),
|
|
)
|
|
|
|
async def async_get_subscription(self, subscription_id: int) -> Subscribe | None:
|
|
"""异步按 ID 查询订阅。"""
|
|
return await self._async_run(
|
|
partial(self.get_subscription, subscription_id)
|
|
)
|
|
|
|
def list_subscription_history(
|
|
self,
|
|
filters: SubscriptionHistoryFilter | dict[str, Any] | None = None,
|
|
page: QueryPageRequest | dict[str, Any] | None = None,
|
|
) -> QueryPage[SubscribeHistory]:
|
|
"""同步分页查询订阅完成历史。"""
|
|
normalized_page = self._page_request(page)
|
|
normalized_filters = self._filter(filters, SubscriptionHistoryFilter)
|
|
rows = self._subscriptions.list_subscription_history(
|
|
filters=normalized_filters,
|
|
page=normalized_page,
|
|
)
|
|
return self._to_page(SubscribeHistory, normalized_page, rows)
|
|
|
|
async def async_list_subscription_history(
|
|
self,
|
|
filters: SubscriptionHistoryFilter | dict[str, Any] | None = None,
|
|
page: QueryPageRequest | dict[str, Any] | None = None,
|
|
) -> QueryPage[SubscribeHistory]:
|
|
"""异步分页查询订阅完成历史。"""
|
|
return await self._async_run(
|
|
partial(self.list_subscription_history, filters, page)
|
|
)
|
|
|
|
def get_subscription_history(self, history_id: int) -> SubscribeHistory | None:
|
|
"""同步按 ID 查询订阅完成历史。"""
|
|
return self._to_item(
|
|
SubscribeHistory,
|
|
self._subscriptions.get_subscription_history(history_id),
|
|
)
|
|
|
|
async def async_get_subscription_history(
|
|
self,
|
|
history_id: int,
|
|
) -> SubscribeHistory | None:
|
|
"""异步按 ID 查询订阅完成历史。"""
|
|
return await self._async_run(
|
|
partial(self.get_subscription_history, history_id)
|
|
)
|
|
|
|
def list_download_history(
|
|
self,
|
|
filters: DownloadHistoryFilter | dict[str, Any] | None = None,
|
|
page: QueryPageRequest | dict[str, Any] | None = None,
|
|
) -> QueryPage[DownloadHistory]:
|
|
"""同步分页查询下载历史。"""
|
|
normalized_page = self._page_request(page)
|
|
normalized_filters = self._filter(filters, DownloadHistoryFilter)
|
|
rows = self._histories.list_download_history(
|
|
filters=normalized_filters,
|
|
page=normalized_page,
|
|
)
|
|
return self._to_page(DownloadHistory, normalized_page, rows)
|
|
|
|
async def async_list_download_history(
|
|
self,
|
|
filters: DownloadHistoryFilter | dict[str, Any] | None = None,
|
|
page: QueryPageRequest | dict[str, Any] | None = None,
|
|
) -> QueryPage[DownloadHistory]:
|
|
"""异步分页查询下载历史。"""
|
|
return await self._async_run(
|
|
partial(self.list_download_history, filters, page)
|
|
)
|
|
|
|
def get_download_history(self, history_id: int) -> DownloadHistory | None:
|
|
"""同步按 ID 查询下载历史。"""
|
|
return self._to_item(
|
|
DownloadHistory,
|
|
self._histories.get_download_history(history_id),
|
|
)
|
|
|
|
async def async_get_download_history(self, history_id: int) -> DownloadHistory | None:
|
|
"""异步按 ID 查询下载历史。"""
|
|
return await self._async_run(
|
|
partial(self.get_download_history, history_id)
|
|
)
|
|
|
|
def list_transfer_history(
|
|
self,
|
|
filters: TransferHistoryFilter | dict[str, Any] | None = None,
|
|
page: QueryPageRequest | dict[str, Any] | None = None,
|
|
) -> QueryPage[TransferHistory]:
|
|
"""同步分页查询整理历史。"""
|
|
normalized_page = self._page_request(page)
|
|
normalized_filters = self._filter(filters, TransferHistoryFilter)
|
|
rows = self._histories.list_transfer_history(
|
|
filters=normalized_filters,
|
|
page=normalized_page,
|
|
)
|
|
return self._to_page(TransferHistory, normalized_page, rows)
|
|
|
|
async def async_list_transfer_history(
|
|
self,
|
|
filters: TransferHistoryFilter | dict[str, Any] | None = None,
|
|
page: QueryPageRequest | dict[str, Any] | None = None,
|
|
) -> QueryPage[TransferHistory]:
|
|
"""异步分页查询整理历史。"""
|
|
return await self._async_run(
|
|
partial(self.list_transfer_history, filters, page)
|
|
)
|
|
|
|
def get_transfer_history(self, history_id: int) -> TransferHistory | None:
|
|
"""同步按 ID 查询整理历史。"""
|
|
return self._to_item(
|
|
TransferHistory,
|
|
self._histories.get_transfer_history(history_id),
|
|
)
|
|
|
|
async def async_get_transfer_history(self, history_id: int) -> TransferHistory | None:
|
|
"""异步按 ID 查询整理历史。"""
|
|
return await self._async_run(
|
|
partial(self.get_transfer_history, history_id)
|
|
)
|
|
|
|
|
|
_configured_data_query_service: DataQueryService | None = None
|
|
|
|
|
|
def configure_data_query_service(service: DataQueryService) -> None:
|
|
"""由启动组合根登记插件数据查询服务。"""
|
|
global _configured_data_query_service
|
|
_configured_data_query_service = service
|
|
|
|
|
|
def get_configured_data_query_service() -> DataQueryService:
|
|
"""返回启动阶段登记的插件数据查询服务。"""
|
|
if _configured_data_query_service is None:
|
|
raise RuntimeError("插件数据查询服务尚未配置")
|
|
return _configured_data_query_service
|
|
|
|
|
|
__all__ = [
|
|
"DataQueryService",
|
|
"HistoryQueryPort",
|
|
"QueryRows",
|
|
"SubscriptionQueryPort",
|
|
"configure_data_query_service",
|
|
"get_configured_data_query_service",
|
|
]
|