refactor(sdk): isolate plugin query snapshots

This commit is contained in:
InfinityPacer
2026-08-27 23:41:54 +08:00
parent 7fa03650bd
commit 6bae137d25
7 changed files with 685 additions and 168 deletions
+49 -52
View File
@@ -10,19 +10,20 @@ from typing import Any, Callable, Generic, Protocol, TypeVar, cast
from pydantic import BaseModel from pydantic import BaseModel
from app.application.database import AsyncDatabaseExecutor from app.application.database import AsyncDatabaseExecutor
from app.schemas.history import DownloadHistory, TransferHistory
from app.schemas.query import ( from app.schemas.query import (
DownloadHistoryFilter, DownloadHistoryFilter,
DownloadHistorySnapshot,
QueryPage, QueryPage,
QueryPageRequest, QueryPageRequest,
SubscribeHistory,
SubscriptionFilter, SubscriptionFilter,
SubscriptionHistoryFilter, SubscriptionHistoryFilter,
SubscriptionHistorySnapshot,
SubscriptionSnapshot,
TransferHistoryFilter, TransferHistoryFilter,
TransferHistorySnapshot,
) )
from app.schemas.subscribe import Subscribe
RecordT = TypeVar("RecordT") RecordT = TypeVar("RecordT", covariant=True)
DtoT = TypeVar("DtoT", bound=BaseModel) DtoT = TypeVar("DtoT", bound=BaseModel)
ResultT = TypeVar("ResultT") ResultT = TypeVar("ResultT")
@@ -176,7 +177,7 @@ class DataQueryService:
self, self,
filters: SubscriptionFilter | dict[str, Any] | None = None, filters: SubscriptionFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[Subscribe]: ) -> QueryPage[SubscriptionSnapshot]:
"""同步分页查询订阅。""" """同步分页查询订阅。"""
normalized_page = self._page_request(page) normalized_page = self._page_request(page)
normalized_filters = self._filter(filters, SubscriptionFilter) normalized_filters = self._filter(filters, SubscriptionFilter)
@@ -184,36 +185,35 @@ class DataQueryService:
filters=normalized_filters, filters=normalized_filters,
page=normalized_page, page=normalized_page,
) )
return self._to_page(Subscribe, normalized_page, rows) return self._to_page(SubscriptionSnapshot, normalized_page, rows)
async def async_list_subscriptions( async def async_list_subscriptions(
self, self,
filters: SubscriptionFilter | dict[str, Any] | None = None, filters: SubscriptionFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[Subscribe]: ) -> QueryPage[SubscriptionSnapshot]:
"""异步分页查询订阅,业务规则在数据库 worker 中复用同步入口。""" """异步分页查询订阅,业务规则在数据库 worker 中复用同步入口。"""
return await self._async_run( return await self._async_run(partial(self.list_subscriptions, filters, page))
partial(self.list_subscriptions, filters, page)
)
def get_subscription(self, subscription_id: int) -> Subscribe | None: def get_subscription(self, subscription_id: int) -> SubscriptionSnapshot | None:
"""同步按 ID 查询订阅。""" """同步按 ID 查询订阅。"""
return self._to_item( return self._to_item(
Subscribe, SubscriptionSnapshot,
self._subscriptions.get_subscription(subscription_id), self._subscriptions.get_subscription(subscription_id),
) )
async def async_get_subscription(self, subscription_id: int) -> Subscribe | None: async def async_get_subscription(
self,
subscription_id: int,
) -> SubscriptionSnapshot | None:
"""异步按 ID 查询订阅。""" """异步按 ID 查询订阅。"""
return await self._async_run( return await self._async_run(partial(self.get_subscription, subscription_id))
partial(self.get_subscription, subscription_id)
)
def list_subscription_history( def list_subscription_history(
self, self,
filters: SubscriptionHistoryFilter | dict[str, Any] | None = None, filters: SubscriptionHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[SubscribeHistory]: ) -> QueryPage[SubscriptionHistorySnapshot]:
"""同步分页查询订阅完成历史。""" """同步分页查询订阅完成历史。"""
normalized_page = self._page_request(page) normalized_page = self._page_request(page)
normalized_filters = self._filter(filters, SubscriptionHistoryFilter) normalized_filters = self._filter(filters, SubscriptionHistoryFilter)
@@ -221,39 +221,38 @@ class DataQueryService:
filters=normalized_filters, filters=normalized_filters,
page=normalized_page, page=normalized_page,
) )
return self._to_page(SubscribeHistory, normalized_page, rows) return self._to_page(SubscriptionHistorySnapshot, normalized_page, rows)
async def async_list_subscription_history( async def async_list_subscription_history(
self, self,
filters: SubscriptionHistoryFilter | dict[str, Any] | None = None, filters: SubscriptionHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[SubscribeHistory]: ) -> QueryPage[SubscriptionHistorySnapshot]:
"""异步分页查询订阅完成历史。""" """异步分页查询订阅完成历史。"""
return await self._async_run( return await self._async_run(partial(self.list_subscription_history, filters, page))
partial(self.list_subscription_history, filters, page)
)
def get_subscription_history(self, history_id: int) -> SubscribeHistory | None: def get_subscription_history(
self,
history_id: int,
) -> SubscriptionHistorySnapshot | None:
"""同步按 ID 查询订阅完成历史。""" """同步按 ID 查询订阅完成历史。"""
return self._to_item( return self._to_item(
SubscribeHistory, SubscriptionHistorySnapshot,
self._subscriptions.get_subscription_history(history_id), self._subscriptions.get_subscription_history(history_id),
) )
async def async_get_subscription_history( async def async_get_subscription_history(
self, self,
history_id: int, history_id: int,
) -> SubscribeHistory | None: ) -> SubscriptionHistorySnapshot | None:
"""异步按 ID 查询订阅完成历史。""" """异步按 ID 查询订阅完成历史。"""
return await self._async_run( return await self._async_run(partial(self.get_subscription_history, history_id))
partial(self.get_subscription_history, history_id)
)
def list_download_history( def list_download_history(
self, self,
filters: DownloadHistoryFilter | dict[str, Any] | None = None, filters: DownloadHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[DownloadHistory]: ) -> QueryPage[DownloadHistorySnapshot]:
"""同步分页查询下载历史。""" """同步分页查询下载历史。"""
normalized_page = self._page_request(page) normalized_page = self._page_request(page)
normalized_filters = self._filter(filters, DownloadHistoryFilter) normalized_filters = self._filter(filters, DownloadHistoryFilter)
@@ -261,36 +260,35 @@ class DataQueryService:
filters=normalized_filters, filters=normalized_filters,
page=normalized_page, page=normalized_page,
) )
return self._to_page(DownloadHistory, normalized_page, rows) return self._to_page(DownloadHistorySnapshot, normalized_page, rows)
async def async_list_download_history( async def async_list_download_history(
self, self,
filters: DownloadHistoryFilter | dict[str, Any] | None = None, filters: DownloadHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[DownloadHistory]: ) -> QueryPage[DownloadHistorySnapshot]:
"""异步分页查询下载历史。""" """异步分页查询下载历史。"""
return await self._async_run( return await self._async_run(partial(self.list_download_history, filters, page))
partial(self.list_download_history, filters, page)
)
def get_download_history(self, history_id: int) -> DownloadHistory | None: def get_download_history(self, history_id: int) -> DownloadHistorySnapshot | None:
"""同步按 ID 查询下载历史。""" """同步按 ID 查询下载历史。"""
return self._to_item( return self._to_item(
DownloadHistory, DownloadHistorySnapshot,
self._histories.get_download_history(history_id), self._histories.get_download_history(history_id),
) )
async def async_get_download_history(self, history_id: int) -> DownloadHistory | None: async def async_get_download_history(
self,
history_id: int,
) -> DownloadHistorySnapshot | None:
"""异步按 ID 查询下载历史。""" """异步按 ID 查询下载历史。"""
return await self._async_run( return await self._async_run(partial(self.get_download_history, history_id))
partial(self.get_download_history, history_id)
)
def list_transfer_history( def list_transfer_history(
self, self,
filters: TransferHistoryFilter | dict[str, Any] | None = None, filters: TransferHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[TransferHistory]: ) -> QueryPage[TransferHistorySnapshot]:
"""同步分页查询整理历史。""" """同步分页查询整理历史。"""
normalized_page = self._page_request(page) normalized_page = self._page_request(page)
normalized_filters = self._filter(filters, TransferHistoryFilter) normalized_filters = self._filter(filters, TransferHistoryFilter)
@@ -298,30 +296,29 @@ class DataQueryService:
filters=normalized_filters, filters=normalized_filters,
page=normalized_page, page=normalized_page,
) )
return self._to_page(TransferHistory, normalized_page, rows) return self._to_page(TransferHistorySnapshot, normalized_page, rows)
async def async_list_transfer_history( async def async_list_transfer_history(
self, self,
filters: TransferHistoryFilter | dict[str, Any] | None = None, filters: TransferHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[TransferHistory]: ) -> QueryPage[TransferHistorySnapshot]:
"""异步分页查询整理历史。""" """异步分页查询整理历史。"""
return await self._async_run( return await self._async_run(partial(self.list_transfer_history, filters, page))
partial(self.list_transfer_history, filters, page)
)
def get_transfer_history(self, history_id: int) -> TransferHistory | None: def get_transfer_history(self, history_id: int) -> TransferHistorySnapshot | None:
"""同步按 ID 查询整理历史。""" """同步按 ID 查询整理历史。"""
return self._to_item( return self._to_item(
TransferHistory, TransferHistorySnapshot,
self._histories.get_transfer_history(history_id), self._histories.get_transfer_history(history_id),
) )
async def async_get_transfer_history(self, history_id: int) -> TransferHistory | None: async def async_get_transfer_history(
self,
history_id: int,
) -> TransferHistorySnapshot | None:
"""异步按 ID 查询整理历史。""" """异步按 ID 查询整理历史。"""
return await self._async_run( return await self._async_run(partial(self.get_transfer_history, history_id))
partial(self.get_transfer_history, history_id)
)
_configured_data_query_service: DataQueryService | None = None _configured_data_query_service: DataQueryService | None = None
+29 -30
View File
@@ -16,23 +16,19 @@ from app.db.models.downloadhistory import DownloadHistory
from app.db.models.subscribe import Subscribe from app.db.models.subscribe import Subscribe
from app.db.models.subscribehistory import SubscribeHistory as SubscribeHistoryModel from app.db.models.subscribehistory import SubscribeHistory as SubscribeHistoryModel
from app.db.models.transferhistory import TransferHistory from app.db.models.transferhistory import TransferHistory
from app.schemas.history import (
DownloadHistory as DownloadHistoryView,
)
from app.schemas.history import (
TransferHistory as TransferHistoryView,
)
from app.schemas.query import ( from app.schemas.query import (
DownloadHistoryFilter, DownloadHistoryFilter,
DownloadHistorySnapshot,
QueryPageRequest, QueryPageRequest,
QuerySortDirection, QuerySortDirection,
QuerySortField, QuerySortField,
SubscriptionFilter, SubscriptionFilter,
SubscriptionHistoryFilter, SubscriptionHistoryFilter,
SubscriptionHistorySnapshot,
SubscriptionSnapshot,
TransferHistoryFilter, TransferHistoryFilter,
TransferHistorySnapshot,
) )
from app.schemas.query import SubscribeHistory as SubscribeHistoryView
from app.schemas.subscribe import Subscribe as SubscribeView
from app.schemas.types import MUSIC_ENTITY_RECORDING from app.schemas.types import MUSIC_ENTITY_RECORDING
_ModelT = TypeVar("_ModelT", bound=Base) _ModelT = TypeVar("_ModelT", bound=Base)
@@ -168,7 +164,7 @@ class SqlAlchemyDataQueryAdapter:
*, *,
filters: SubscriptionFilter, filters: SubscriptionFilter,
page: QueryPageRequest, page: QueryPageRequest,
) -> QueryRows[SubscribeView]: ) -> QueryRows[SubscriptionSnapshot]:
"""按受控组合条件分页查询当前订阅。""" """按受控组合条件分页查询当前订阅。"""
query = filters query = filters
conditions = self._identity_conditions(Subscribe, query) conditions = self._identity_conditions(Subscribe, query)
@@ -196,16 +192,16 @@ class SqlAlchemyDataQueryAdapter:
conditions.append(music_condition) conditions.append(music_condition)
return self._page( return self._page(
model=Subscribe, model=Subscribe,
view_model=SubscribeView, view_model=SubscriptionSnapshot,
conditions=conditions, conditions=conditions,
page=page, page=page,
) )
def get_subscription(self, subscription_id: int) -> SubscribeView | None: def get_subscription(self, subscription_id: int) -> SubscriptionSnapshot | None:
"""按主键查询订阅并返回脱离 Session 的 DTO。""" """按主键查询订阅并返回脱离 Session 的 DTO。"""
return self._get( return self._get(
model=Subscribe, model=Subscribe,
view_model=SubscribeView, view_model=SubscriptionSnapshot,
record_id=subscription_id, record_id=subscription_id,
) )
@@ -214,7 +210,7 @@ class SqlAlchemyDataQueryAdapter:
*, *,
filters: SubscriptionHistoryFilter, filters: SubscriptionHistoryFilter,
page: QueryPageRequest, page: QueryPageRequest,
) -> QueryRows[SubscribeHistoryView]: ) -> QueryRows[SubscriptionHistorySnapshot]:
"""按受控组合条件分页查询订阅完成历史。""" """按受控组合条件分页查询订阅完成历史。"""
query = filters query = filters
conditions = self._identity_conditions(SubscribeHistoryModel, query) conditions = self._identity_conditions(SubscribeHistoryModel, query)
@@ -242,7 +238,7 @@ class SqlAlchemyDataQueryAdapter:
conditions.append(music_condition) conditions.append(music_condition)
return self._page( return self._page(
model=SubscribeHistoryModel, model=SubscribeHistoryModel,
view_model=SubscribeHistoryView, view_model=SubscriptionHistorySnapshot,
conditions=conditions, conditions=conditions,
page=page, page=page,
) )
@@ -250,11 +246,11 @@ class SqlAlchemyDataQueryAdapter:
def get_subscription_history( def get_subscription_history(
self, self,
history_id: int, history_id: int,
) -> SubscribeHistoryView | None: ) -> SubscriptionHistorySnapshot | None:
"""按主键查询订阅完成历史并返回稳定 DTO。""" """按主键查询订阅完成历史并返回稳定 DTO。"""
return self._get( return self._get(
model=SubscribeHistoryModel, model=SubscribeHistoryModel,
view_model=SubscribeHistoryView, view_model=SubscriptionHistorySnapshot,
record_id=history_id, record_id=history_id,
) )
@@ -263,7 +259,7 @@ class SqlAlchemyDataQueryAdapter:
*, *,
filters: DownloadHistoryFilter, filters: DownloadHistoryFilter,
page: QueryPageRequest, page: QueryPageRequest,
) -> QueryRows[DownloadHistoryView]: ) -> QueryRows[DownloadHistorySnapshot]:
"""按受控组合条件分页查询下载历史。""" """按受控组合条件分页查询下载历史。"""
query = filters query = filters
conditions = self._identity_conditions(DownloadHistory, query) conditions = self._identity_conditions(DownloadHistory, query)
@@ -276,20 +272,23 @@ class SqlAlchemyDataQueryAdapter:
conditions.append(DownloadHistory.type.in_(media_types)) conditions.append(DownloadHistory.type.in_(media_types))
for column, value in ( for column, value in (
(DownloadHistory.title, query.title), (DownloadHistory.title, query.title),
(DownloadHistory.path, query.path),
):
if value:
conditions.append(_contains(column, value))
for column, value in (
(DownloadHistory.year, query.year), (DownloadHistory.year, query.year),
(DownloadHistory.seasons, query.seasons), (DownloadHistory.seasons, query.seasons),
(DownloadHistory.episodes, query.episodes), (DownloadHistory.episodes, query.episodes),
(DownloadHistory.path, query.path),
(DownloadHistory.download_hash, query.download_hash), (DownloadHistory.download_hash, query.download_hash),
(DownloadHistory.username, query.username), (DownloadHistory.username, query.username),
(DownloadHistory.episode_group, query.episode_group), (DownloadHistory.episode_group, query.episode_group),
): ):
if value is not None and value != "": if value is not None and value != "":
conditions.append(column == value) conditions.append(column == value)
if query.text:
conditions.append(
or_(
_contains(DownloadHistory.title, query.text),
_contains(DownloadHistory.path, query.text),
)
)
if usernames: if usernames:
conditions.append(DownloadHistory.username.in_(usernames)) conditions.append(DownloadHistory.username.in_(usernames))
music_condition = _music_type_condition(DownloadHistory.music_type, query.music_type) music_condition = _music_type_condition(DownloadHistory.music_type, query.music_type)
@@ -297,16 +296,16 @@ class SqlAlchemyDataQueryAdapter:
conditions.append(music_condition) conditions.append(music_condition)
return self._page( return self._page(
model=DownloadHistory, model=DownloadHistory,
view_model=DownloadHistoryView, view_model=DownloadHistorySnapshot,
conditions=conditions, conditions=conditions,
page=page, page=page,
) )
def get_download_history(self, history_id: int) -> DownloadHistoryView | None: def get_download_history(self, history_id: int) -> DownloadHistorySnapshot | None:
"""按主键查询下载历史并返回稳定 DTO。""" """按主键查询下载历史并返回稳定 DTO。"""
return self._get( return self._get(
model=DownloadHistory, model=DownloadHistory,
view_model=DownloadHistoryView, view_model=DownloadHistorySnapshot,
record_id=history_id, record_id=history_id,
) )
@@ -315,7 +314,7 @@ class SqlAlchemyDataQueryAdapter:
*, *,
filters: TransferHistoryFilter, filters: TransferHistoryFilter,
page: QueryPageRequest, page: QueryPageRequest,
) -> QueryRows[TransferHistoryView]: ) -> QueryRows[TransferHistorySnapshot]:
"""按受控组合条件分页查询整理历史。""" """按受控组合条件分页查询整理历史。"""
query = filters query = filters
conditions = self._identity_conditions(TransferHistory, query) conditions = self._identity_conditions(TransferHistory, query)
@@ -331,7 +330,7 @@ class SqlAlchemyDataQueryAdapter:
if query.require_media_identity: if query.require_media_identity:
conditions.extend(self._require_media_identity(TransferHistory)) conditions.extend(self._require_media_identity(TransferHistory))
if query.title: if query.title:
conditions.append(_contains(TransferHistory.title, query.title)) conditions.append(TransferHistory.title == query.title)
if query.text: if query.text:
conditions.append( conditions.append(
or_( or_(
@@ -358,16 +357,16 @@ class SqlAlchemyDataQueryAdapter:
conditions.append(music_condition) conditions.append(music_condition)
return self._page( return self._page(
model=TransferHistory, model=TransferHistory,
view_model=TransferHistoryView, view_model=TransferHistorySnapshot,
conditions=conditions, conditions=conditions,
page=page, page=page,
) )
def get_transfer_history(self, history_id: int) -> TransferHistoryView | None: def get_transfer_history(self, history_id: int) -> TransferHistorySnapshot | None:
"""按主键查询整理历史并返回稳定 DTO。""" """按主键查询整理历史并返回稳定 DTO。"""
return self._get( return self._get(
model=TransferHistory, model=TransferHistory,
view_model=TransferHistoryView, view_model=TransferHistorySnapshot,
record_id=history_id, record_id=history_id,
) )
+164 -18
View File
@@ -1,14 +1,12 @@
"""插件只读数据查询使用的稳定筛选、分页与数据投影合同。""" """插件只读数据查询使用的稳定筛选、分页与数据投影合同。"""
from enum import Enum from enum import Enum
from typing import Generic, Optional, TypeVar from typing import Any, Generic, Optional, TypeVar
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from app.schemas.common import JsonData from app.schemas.common import JsonData
from app.schemas.history import DownloadHistory, TransferHistory from app.schemas.media import OptionalMediaIdentityMixin, normalize_media_source
from app.schemas.media import OptionalMediaIdentityMixin
from app.schemas.subscribe import Subscribe
from app.schemas.types import MediaSource, MediaType from app.schemas.types import MediaSource, MediaType
T = TypeVar("T") T = TypeVar("T")
@@ -17,7 +15,7 @@ DEFAULT_QUERY_PAGE_SIZE = 50
MAX_QUERY_PAGE_SIZE = 200 MAX_QUERY_PAGE_SIZE = 200
class _QueryInput(BaseModel): class _QueryInput(BaseModel): # type: ignore[misc] # Pydantic imports are skipped by strict mypy
"""查询输入共同的严格字段合同。""" """查询输入共同的严格字段合同。"""
model_config = ConfigDict(extra="forbid") model_config = ConfigDict(extra="forbid")
@@ -56,7 +54,7 @@ class QueryPageRequest(_QueryInput):
sort: QuerySort = Field(default_factory=QuerySort) sort: QuerySort = Field(default_factory=QuerySort)
class QueryPage(BaseModel, Generic[T]): class QueryPage(BaseModel, Generic[T]): # type: ignore[misc] # Pydantic imports are skipped by strict mypy
"""公开查询返回的分页 DTO。""" """公开查询返回的分页 DTO。"""
items: list[T] = Field(default_factory=list) items: list[T] = Field(default_factory=list)
@@ -74,23 +72,105 @@ class QueryPage(BaseModel, Generic[T]):
return self.page * self.count < self.total return self.page * self.count < self.total
class MediaIdentityQuery(OptionalMediaIdentityMixin, _QueryInput): class MediaIdentityQuery(
OptionalMediaIdentityMixin,
_QueryInput,
):
"""允许省略身份,但显式筛选时要求来源与原生 ID 成对有效。""" """允许省略身份,但显式筛选时要求来源与原生 ID 成对有效。"""
media_source: Optional[MediaSource] = None media_source: Optional[MediaSource] = None
media_id: Optional[str] = None media_id: Optional[str] = None
class SubscribeHistory(OptionalMediaIdentityMixin, BaseModel): class _QuerySnapshot(BaseModel): # type: ignore[misc] # Pydantic imports are skipped by strict mypy
"""订阅完成历史的稳定只读投影""" """只读查询 DTO 的媒体身份与脏数据归一化边界"""
media_source: Optional[MediaSource] = None
media_id: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
@field_validator("media_source", mode="before") # type: ignore[misc]
@classmethod
def _normalize_source(cls, value: Any) -> Optional[MediaSource]:
"""未知旧来源不应使整页查询失败,统一降级为空身份。"""
return normalize_media_source(value)
@model_validator(mode="after") # type: ignore[misc]
def _normalize_identity_pair(self) -> "_QuerySnapshot":
"""输出中的脏半对身份按无身份处理,避免向插件传播不可用主键。"""
normalized_id = str(self.media_id).strip() if self.media_id is not None else ""
if not self.media_source or not normalized_id or normalized_id == "0":
object.__setattr__(self, "media_source", None)
object.__setattr__(self, "media_id", None)
else:
object.__setattr__(self, "media_id", normalized_id)
return self
class SubscriptionSnapshot(_QuerySnapshot):
"""当前订阅的稳定只读快照,不包含 ORM 或写入行为。"""
id: int
name: Optional[str] = None
year: Optional[str] = None
type: Optional[str] = None
keyword: Optional[str] = None
music_type: Optional[str] = None
total_tracks: Optional[int] = None
season: Optional[int] = None
poster: Optional[str] = None
backdrop: Optional[str] = None
vote: Optional[float] = None
description: Optional[str] = None
# 资源选择规则与质量约束均按宿主保存值只读返回。
filter: Optional[str] = None
include: Optional[str] = None
exclude: Optional[str] = None
quality: Optional[str] = None
resolution: Optional[str] = None
effect: Optional[str] = None
audio_quality: Optional[str] = None
audio_format: Optional[str] = None
min_bitrate: Optional[int] = None
min_bit_depth: Optional[int] = None
min_sample_rate: Optional[int] = None
# 订阅进度字段由宿主维护,插件查询不得据此直接写库。
total_episode: Optional[int] = None
start_episode: Optional[int] = None
lack_episode: Optional[int] = None
note: Optional[JsonData] = None
state: Optional[str] = None
last_update: Optional[str] = None
date: Optional[str] = None
username: Optional[str] = None
sites: Optional[list[int]] = None
downloader: Optional[str] = None
best_version: Optional[int] = None
best_version_full: Optional[int] = None
current_priority: Optional[int] = None
current_audio_format: Optional[str] = None
current_bitrate: Optional[int] = None
current_bit_depth: Optional[int] = None
current_sample_rate: Optional[int] = None
episode_priority: Optional[dict[str, int]] = None
save_path: Optional[str] = None
search_imdbid: Optional[int] = None
manual_total_episode: Optional[int] = None
custom_words: Optional[str] = None
media_category: Optional[str] = None
filter_groups: Optional[list[str]] = None
episode_group: Optional[str] = None
class SubscriptionHistorySnapshot(_QuerySnapshot):
"""订阅完成历史的稳定只读快照。"""
id: int id: int
name: Optional[str] = None name: Optional[str] = None
year: Optional[str] = None year: Optional[str] = None
type: Optional[str] = None type: Optional[str] = None
keyword: Optional[str] = None keyword: Optional[str] = None
media_source: Optional[MediaSource] = None
media_id: Optional[str] = None
music_type: Optional[str] = None music_type: Optional[str] = None
total_tracks: Optional[int] = None total_tracks: Optional[int] = None
season: Optional[int] = None season: Optional[int] = None
@@ -124,13 +204,78 @@ class SubscribeHistory(OptionalMediaIdentityMixin, BaseModel):
episode_priority: Optional[dict[str, int]] = None episode_priority: Optional[dict[str, int]] = None
save_path: Optional[str] = None save_path: Optional[str] = None
search_imdbid: Optional[int] = None search_imdbid: Optional[int] = None
note: Optional[JsonData] = None
custom_words: Optional[str] = None custom_words: Optional[str] = None
media_category: Optional[str] = None media_category: Optional[str] = None
filter_groups: Optional[list[str]] = None filter_groups: Optional[list[str]] = None
episode_group: Optional[str] = None episode_group: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
class DownloadHistorySnapshot(_QuerySnapshot):
"""下载历史的稳定只读快照。"""
id: int
path: Optional[str] = None
type: Optional[str] = None
title: Optional[str] = None
year: Optional[str] = None
music_type: Optional[str] = None
seasons: Optional[str] = None
episodes: Optional[str] = None
image: Optional[str] = None
poster: Optional[str] = None
downloader: Optional[str] = None
download_hash: Optional[str] = None
torrent_name: Optional[str] = None
torrent_description: Optional[str] = None
torrent_site: Optional[str] = None
userid: Optional[str] = None
username: Optional[str] = None
channel: Optional[str] = None
date: Optional[str] = None
note: Optional[JsonData] = None
media_category: Optional[str] = None
episode_group: Optional[str] = None
custom_words: Optional[str] = None
class TransferHistorySnapshot(_QuerySnapshot):
"""整理历史的稳定只读快照;内部任务结算标识不会暴露给插件。"""
id: int
src: Optional[str] = None
src_storage: Optional[str] = None
src_fileitem: Optional[JsonData] = None
dest: Optional[str] = None
dest_storage: Optional[str] = None
dest_fileitem: Optional[JsonData] = None
mode: Optional[str] = None
type: Optional[str] = None
category: Optional[str] = None
title: Optional[str] = None
year: Optional[str] = None
music_type: Optional[str] = None
total_tracks: Optional[int] = None
audio_format: Optional[str] = None
audio_lossless: Optional[bool] = None
bit_depth: Optional[int] = None
sample_rate: Optional[int] = None
bitrate: Optional[int] = None
seasons: Optional[str] = None
episodes: Optional[str] = None
image: Optional[str] = None
downloader: Optional[str] = None
download_hash: Optional[str] = None
status: bool = False
errmsg: Optional[str] = None
date: Optional[str] = None
files: Optional[JsonData] = None
episode_group: Optional[str] = None
@field_validator("status", mode="before") # type: ignore[misc]
@classmethod
def _normalize_status(cls, value: object) -> bool:
"""旧记录的 NULL 状态按失败处理,避免错误地向插件声明整理成功。"""
return bool(value)
class SubscriptionFilter(MediaIdentityQuery): class SubscriptionFilter(MediaIdentityQuery):
@@ -164,6 +309,7 @@ class DownloadHistoryFilter(MediaIdentityQuery):
ids: tuple[int, ...] = () ids: tuple[int, ...] = ()
media_types: tuple[MediaType, ...] = () media_types: tuple[MediaType, ...] = ()
title: Optional[str] = None title: Optional[str] = None
text: Optional[str] = None
year: Optional[str] = None year: Optional[str] = None
seasons: Optional[str] = None seasons: Optional[str] = None
episodes: Optional[str] = None episodes: Optional[str] = None
@@ -198,18 +344,18 @@ class TransferHistoryFilter(MediaIdentityQuery):
__all__ = [ __all__ = [
"DEFAULT_QUERY_PAGE_SIZE", "DEFAULT_QUERY_PAGE_SIZE",
"MAX_QUERY_PAGE_SIZE", "MAX_QUERY_PAGE_SIZE",
"DownloadHistory",
"DownloadHistoryFilter", "DownloadHistoryFilter",
"DownloadHistorySnapshot",
"MediaIdentityQuery", "MediaIdentityQuery",
"QueryPage", "QueryPage",
"QueryPageRequest", "QueryPageRequest",
"QuerySort", "QuerySort",
"QuerySortDirection", "QuerySortDirection",
"QuerySortField", "QuerySortField",
"Subscribe",
"SubscribeHistory",
"SubscriptionFilter", "SubscriptionFilter",
"SubscriptionHistorySnapshot",
"SubscriptionHistoryFilter", "SubscriptionHistoryFilter",
"TransferHistory", "SubscriptionSnapshot",
"TransferHistoryFilter", "TransferHistoryFilter",
"TransferHistorySnapshot",
] ]
+127 -27
View File
@@ -2,41 +2,133 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any, Protocol, cast
from app.schemas.query import ( from app.schemas.query import (
DEFAULT_QUERY_PAGE_SIZE, DEFAULT_QUERY_PAGE_SIZE,
MAX_QUERY_PAGE_SIZE, MAX_QUERY_PAGE_SIZE,
DownloadHistory,
DownloadHistoryFilter, DownloadHistoryFilter,
DownloadHistorySnapshot,
MediaIdentityQuery, MediaIdentityQuery,
QueryPage, QueryPage,
QueryPageRequest, QueryPageRequest,
QuerySort, QuerySort,
QuerySortDirection, QuerySortDirection,
QuerySortField, QuerySortField,
Subscribe,
SubscribeHistory,
SubscriptionFilter, SubscriptionFilter,
SubscriptionHistoryFilter, SubscriptionHistoryFilter,
TransferHistory, SubscriptionHistorySnapshot,
SubscriptionSnapshot,
TransferHistoryFilter, TransferHistoryFilter,
TransferHistorySnapshot,
) )
def _service() -> Any: class _DataQueryBackend(Protocol):
"""SDK 转发所需的最小类型合同,不向插件公开应用服务实现。"""
def list_subscriptions(
self,
filters: SubscriptionFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[SubscriptionSnapshot]: ...
async def async_list_subscriptions(
self,
filters: SubscriptionFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[SubscriptionSnapshot]: ...
def get_subscription(
self,
subscription_id: int,
) -> SubscriptionSnapshot | None: ...
async def async_get_subscription(
self,
subscription_id: int,
) -> SubscriptionSnapshot | None: ...
def list_subscription_history(
self,
filters: SubscriptionHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[SubscriptionHistorySnapshot]: ...
async def async_list_subscription_history(
self,
filters: SubscriptionHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[SubscriptionHistorySnapshot]: ...
def get_subscription_history(
self,
history_id: int,
) -> SubscriptionHistorySnapshot | None: ...
async def async_get_subscription_history(
self,
history_id: int,
) -> SubscriptionHistorySnapshot | None: ...
def list_download_history(
self,
filters: DownloadHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[DownloadHistorySnapshot]: ...
async def async_list_download_history(
self,
filters: DownloadHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[DownloadHistorySnapshot]: ...
def get_download_history(
self,
history_id: int,
) -> DownloadHistorySnapshot | None: ...
async def async_get_download_history(
self,
history_id: int,
) -> DownloadHistorySnapshot | None: ...
def list_transfer_history(
self,
filters: TransferHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[TransferHistorySnapshot]: ...
async def async_list_transfer_history(
self,
filters: TransferHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[TransferHistorySnapshot]: ...
def get_transfer_history(
self,
history_id: int,
) -> TransferHistorySnapshot | None: ...
async def async_get_transfer_history(
self,
history_id: int,
) -> TransferHistorySnapshot | None: ...
def _service() -> _DataQueryBackend:
"""获取启动阶段登记的查询服务,避免把应用服务暴露为 SDK 合同。""" """获取启动阶段登记的查询服务,避免把应用服务暴露为 SDK 合同。"""
from app.application.data_query import ( from app.application.data_query import (
get_configured_data_query_service, get_configured_data_query_service,
) )
return get_configured_data_query_service() return cast(_DataQueryBackend, get_configured_data_query_service())
def list_subscriptions( def list_subscriptions(
filters: SubscriptionFilter | dict[str, Any] | None = None, filters: SubscriptionFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[Subscribe]: ) -> QueryPage[SubscriptionSnapshot]:
"""同步分页读取订阅 DTO。""" """同步分页读取订阅 DTO。"""
return _service().list_subscriptions(filters, page) return _service().list_subscriptions(filters, page)
@@ -44,17 +136,19 @@ def list_subscriptions(
async def async_list_subscriptions( async def async_list_subscriptions(
filters: SubscriptionFilter | dict[str, Any] | None = None, filters: SubscriptionFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[Subscribe]: ) -> QueryPage[SubscriptionSnapshot]:
"""异步分页读取订阅 DTO。""" """异步分页读取订阅 DTO。"""
return await _service().async_list_subscriptions(filters, page) return await _service().async_list_subscriptions(filters, page)
def get_subscription(subscription_id: int) -> Subscribe | None: def get_subscription(subscription_id: int) -> SubscriptionSnapshot | None:
"""同步按 ID 读取订阅 DTO。""" """同步按 ID 读取订阅 DTO。"""
return _service().get_subscription(subscription_id) return _service().get_subscription(subscription_id)
async def async_get_subscription(subscription_id: int) -> Subscribe | None: async def async_get_subscription(
subscription_id: int,
) -> SubscriptionSnapshot | None:
"""异步按 ID 读取订阅 DTO。""" """异步按 ID 读取订阅 DTO。"""
return await _service().async_get_subscription(subscription_id) return await _service().async_get_subscription(subscription_id)
@@ -62,7 +156,7 @@ async def async_get_subscription(subscription_id: int) -> Subscribe | None:
def list_subscription_history( def list_subscription_history(
filters: SubscriptionHistoryFilter | dict[str, Any] | None = None, filters: SubscriptionHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[SubscribeHistory]: ) -> QueryPage[SubscriptionHistorySnapshot]:
"""同步分页读取订阅完成历史 DTO。""" """同步分页读取订阅完成历史 DTO。"""
return _service().list_subscription_history(filters, page) return _service().list_subscription_history(filters, page)
@@ -70,17 +164,19 @@ def list_subscription_history(
async def async_list_subscription_history( async def async_list_subscription_history(
filters: SubscriptionHistoryFilter | dict[str, Any] | None = None, filters: SubscriptionHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[SubscribeHistory]: ) -> QueryPage[SubscriptionHistorySnapshot]:
"""异步分页读取订阅完成历史 DTO。""" """异步分页读取订阅完成历史 DTO。"""
return await _service().async_list_subscription_history(filters, page) return await _service().async_list_subscription_history(filters, page)
def get_subscription_history(history_id: int) -> SubscribeHistory | None: def get_subscription_history(history_id: int) -> SubscriptionHistorySnapshot | None:
"""同步按 ID 读取订阅完成历史 DTO。""" """同步按 ID 读取订阅完成历史 DTO。"""
return _service().get_subscription_history(history_id) return _service().get_subscription_history(history_id)
async def async_get_subscription_history(history_id: int) -> SubscribeHistory | None: async def async_get_subscription_history(
history_id: int,
) -> SubscriptionHistorySnapshot | None:
"""异步按 ID 读取订阅完成历史 DTO。""" """异步按 ID 读取订阅完成历史 DTO。"""
return await _service().async_get_subscription_history(history_id) return await _service().async_get_subscription_history(history_id)
@@ -88,7 +184,7 @@ async def async_get_subscription_history(history_id: int) -> SubscribeHistory |
def list_download_history( def list_download_history(
filters: DownloadHistoryFilter | dict[str, Any] | None = None, filters: DownloadHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[DownloadHistory]: ) -> QueryPage[DownloadHistorySnapshot]:
"""同步分页读取下载历史 DTO。""" """同步分页读取下载历史 DTO。"""
return _service().list_download_history(filters, page) return _service().list_download_history(filters, page)
@@ -96,17 +192,19 @@ def list_download_history(
async def async_list_download_history( async def async_list_download_history(
filters: DownloadHistoryFilter | dict[str, Any] | None = None, filters: DownloadHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[DownloadHistory]: ) -> QueryPage[DownloadHistorySnapshot]:
"""异步分页读取下载历史 DTO。""" """异步分页读取下载历史 DTO。"""
return await _service().async_list_download_history(filters, page) return await _service().async_list_download_history(filters, page)
def get_download_history(history_id: int) -> DownloadHistory | None: def get_download_history(history_id: int) -> DownloadHistorySnapshot | None:
"""同步按 ID 读取下载历史 DTO。""" """同步按 ID 读取下载历史 DTO。"""
return _service().get_download_history(history_id) return _service().get_download_history(history_id)
async def async_get_download_history(history_id: int) -> DownloadHistory | None: async def async_get_download_history(
history_id: int,
) -> DownloadHistorySnapshot | None:
"""异步按 ID 读取下载历史 DTO。""" """异步按 ID 读取下载历史 DTO。"""
return await _service().async_get_download_history(history_id) return await _service().async_get_download_history(history_id)
@@ -114,7 +212,7 @@ async def async_get_download_history(history_id: int) -> DownloadHistory | None:
def list_transfer_history( def list_transfer_history(
filters: TransferHistoryFilter | dict[str, Any] | None = None, filters: TransferHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[TransferHistory]: ) -> QueryPage[TransferHistorySnapshot]:
"""同步分页读取整理历史 DTO。""" """同步分页读取整理历史 DTO。"""
return _service().list_transfer_history(filters, page) return _service().list_transfer_history(filters, page)
@@ -122,17 +220,19 @@ def list_transfer_history(
async def async_list_transfer_history( async def async_list_transfer_history(
filters: TransferHistoryFilter | dict[str, Any] | None = None, filters: TransferHistoryFilter | dict[str, Any] | None = None,
page: QueryPageRequest | dict[str, Any] | None = None, page: QueryPageRequest | dict[str, Any] | None = None,
) -> QueryPage[TransferHistory]: ) -> QueryPage[TransferHistorySnapshot]:
"""异步分页读取整理历史 DTO。""" """异步分页读取整理历史 DTO。"""
return await _service().async_list_transfer_history(filters, page) return await _service().async_list_transfer_history(filters, page)
def get_transfer_history(history_id: int) -> TransferHistory | None: def get_transfer_history(history_id: int) -> TransferHistorySnapshot | None:
"""同步按 ID 读取整理历史 DTO。""" """同步按 ID 读取整理历史 DTO。"""
return _service().get_transfer_history(history_id) return _service().get_transfer_history(history_id)
async def async_get_transfer_history(history_id: int) -> TransferHistory | None: async def async_get_transfer_history(
history_id: int,
) -> TransferHistorySnapshot | None:
"""异步按 ID 读取整理历史 DTO。""" """异步按 ID 读取整理历史 DTO。"""
return await _service().async_get_transfer_history(history_id) return await _service().async_get_transfer_history(history_id)
@@ -140,20 +240,20 @@ async def async_get_transfer_history(history_id: int) -> TransferHistory | None:
__all__ = [ __all__ = [
"DEFAULT_QUERY_PAGE_SIZE", "DEFAULT_QUERY_PAGE_SIZE",
"MAX_QUERY_PAGE_SIZE", "MAX_QUERY_PAGE_SIZE",
"DownloadHistory",
"DownloadHistoryFilter", "DownloadHistoryFilter",
"DownloadHistorySnapshot",
"MediaIdentityQuery", "MediaIdentityQuery",
"QueryPage", "QueryPage",
"QueryPageRequest", "QueryPageRequest",
"QuerySort", "QuerySort",
"QuerySortDirection", "QuerySortDirection",
"QuerySortField", "QuerySortField",
"Subscribe",
"SubscribeHistory",
"SubscriptionFilter", "SubscriptionFilter",
"SubscriptionHistoryFilter", "SubscriptionHistoryFilter",
"TransferHistory", "SubscriptionHistorySnapshot",
"SubscriptionSnapshot",
"TransferHistoryFilter", "TransferHistoryFilter",
"TransferHistorySnapshot",
"async_get_download_history", "async_get_download_history",
"async_get_subscription", "async_get_subscription",
"async_get_subscription_history", "async_get_subscription_history",
+33 -3
View File
@@ -1441,8 +1441,8 @@
"runtime_only": true "runtime_only": true
} }
}, },
"edge_count": 6898, "edge_count": 6924,
"edge_sha256": "c73faf7e29ee12a862cf2332f1803fd4eb8a3f4d2994ed81075ff198e896f8df", "edge_sha256": "d5471439086bcfdd8060b77670af6f821a34e3a864ba7e2cb7230c599a1cb3bb",
"edges": [ "edges": [
"app -> app.runtime", "app -> app.runtime",
"app -> app.runtime.compat", "app -> app.runtime.compat",
@@ -4008,6 +4008,10 @@
"app.application.configuration -> app.schemas.types", "app.application.configuration -> app.schemas.types",
"app.application.dashboard -> app.schemas", "app.application.dashboard -> app.schemas",
"app.application.dashboard -> app.schemas.dashboard", "app.application.dashboard -> app.schemas.dashboard",
"app.application.data_query -> app.application",
"app.application.data_query -> app.application.database",
"app.application.data_query -> app.schemas",
"app.application.data_query -> app.schemas.query",
"app.application.directory -> app.adapters", "app.application.directory -> app.adapters",
"app.application.directory -> app.adapters.system", "app.application.directory -> app.adapters.system",
"app.application.directory -> app.adapters.system.host", "app.application.directory -> app.adapters.system.host",
@@ -5131,6 +5135,18 @@
"app.db.adapters.chain -> app.db.oper.transferpending", "app.db.adapters.chain -> app.db.oper.transferpending",
"app.db.adapters.chain -> app.db.oper.transfersettlementreceipt", "app.db.adapters.chain -> app.db.oper.transfersettlementreceipt",
"app.db.adapters.chain -> app.db.uow", "app.db.adapters.chain -> app.db.uow",
"app.db.adapters.data_query -> app.application",
"app.db.adapters.data_query -> app.application.data_query",
"app.db.adapters.data_query -> app.db",
"app.db.adapters.data_query -> app.db.base",
"app.db.adapters.data_query -> app.db.models",
"app.db.adapters.data_query -> app.db.models.downloadhistory",
"app.db.adapters.data_query -> app.db.models.subscribe",
"app.db.adapters.data_query -> app.db.models.subscribehistory",
"app.db.adapters.data_query -> app.db.models.transferhistory",
"app.db.adapters.data_query -> app.schemas",
"app.db.adapters.data_query -> app.schemas.query",
"app.db.adapters.data_query -> app.schemas.types",
"app.db.adapters.download -> app.db", "app.db.adapters.download -> app.db",
"app.db.adapters.download -> app.db.oper", "app.db.adapters.download -> app.db.oper",
"app.db.adapters.download -> app.db.oper.downloadfailure", "app.db.adapters.download -> app.db.oper.downloadfailure",
@@ -7629,6 +7645,10 @@
"app.schemas.openai -> app.schemas.common", "app.schemas.openai -> app.schemas.common",
"app.schemas.plugin -> app.schemas", "app.schemas.plugin -> app.schemas",
"app.schemas.plugin -> app.schemas.common", "app.schemas.plugin -> app.schemas.common",
"app.schemas.query -> app.schemas",
"app.schemas.query -> app.schemas.common",
"app.schemas.query -> app.schemas.media",
"app.schemas.query -> app.schemas.types",
"app.schemas.response -> app.runtime", "app.schemas.response -> app.runtime",
"app.schemas.response -> app.runtime.localization", "app.schemas.response -> app.runtime.localization",
"app.schemas.search -> app.schemas", "app.schemas.search -> app.schemas",
@@ -7748,6 +7768,10 @@
"app.sdk.plugins -> app.runtime.extensions", "app.sdk.plugins -> app.runtime.extensions",
"app.sdk.plugins -> app.runtime.extensions.module_manager", "app.sdk.plugins -> app.runtime.extensions.module_manager",
"app.sdk.plugins -> app.runtime.extensions.plugin_manager", "app.sdk.plugins -> app.runtime.extensions.plugin_manager",
"app.sdk.queries -> app.application",
"app.sdk.queries -> app.application.data_query",
"app.sdk.queries -> app.schemas",
"app.sdk.queries -> app.schemas.query",
"app.sdk.security -> app.adapters", "app.sdk.security -> app.adapters",
"app.sdk.security -> app.adapters.web", "app.sdk.security -> app.adapters.web",
"app.sdk.security -> app.adapters.web.security", "app.sdk.security -> app.adapters.web.security",
@@ -7929,6 +7953,7 @@
"app.startup.initializers.modules -> app.application.chain.data", "app.startup.initializers.modules -> app.application.chain.data",
"app.startup.initializers.modules -> app.application.chain.events", "app.startup.initializers.modules -> app.application.chain.events",
"app.startup.initializers.modules -> app.application.configuration", "app.startup.initializers.modules -> app.application.configuration",
"app.startup.initializers.modules -> app.application.data_query",
"app.startup.initializers.modules -> app.application.database", "app.startup.initializers.modules -> app.application.database",
"app.startup.initializers.modules -> app.application.history", "app.startup.initializers.modules -> app.application.history",
"app.startup.initializers.modules -> app.application.image", "app.startup.initializers.modules -> app.application.image",
@@ -7971,6 +7996,7 @@
"app.startup.initializers.modules -> app.db", "app.startup.initializers.modules -> app.db",
"app.startup.initializers.modules -> app.db.adapters", "app.startup.initializers.modules -> app.db.adapters",
"app.startup.initializers.modules -> app.db.adapters.chain", "app.startup.initializers.modules -> app.db.adapters.chain",
"app.startup.initializers.modules -> app.db.adapters.data_query",
"app.startup.initializers.modules -> app.db.adapters.download", "app.startup.initializers.modules -> app.db.adapters.download",
"app.startup.initializers.modules -> app.db.adapters.outbox", "app.startup.initializers.modules -> app.db.adapters.outbox",
"app.startup.initializers.modules -> app.db.adapters.pluginidentity", "app.startup.initializers.modules -> app.db.adapters.pluginidentity",
@@ -8343,7 +8369,7 @@
"app.workflow.actions.transfer_file -> app.workflow", "app.workflow.actions.transfer_file -> app.workflow",
"app.workflow.actions.transfer_file -> app.workflow.actions" "app.workflow.actions.transfer_file -> app.workflow.actions"
], ],
"module_count": 844, "module_count": 848,
"modules": [ "modules": [
"app", "app",
"app.adapters", "app.adapters",
@@ -8603,6 +8629,7 @@
"app.application.commands", "app.application.commands",
"app.application.configuration", "app.application.configuration",
"app.application.dashboard", "app.application.dashboard",
"app.application.data_query",
"app.application.database", "app.application.database",
"app.application.directory", "app.application.directory",
"app.application.download", "app.application.download",
@@ -8735,6 +8762,7 @@
"app.db", "app.db",
"app.db.adapters", "app.db.adapters",
"app.db.adapters.chain", "app.db.adapters.chain",
"app.db.adapters.data_query",
"app.db.adapters.download", "app.db.adapters.download",
"app.db.adapters.outbox", "app.db.adapters.outbox",
"app.db.adapters.pluginidentity", "app.db.adapters.pluginidentity",
@@ -9111,6 +9139,7 @@
"app.schemas.notification", "app.schemas.notification",
"app.schemas.openai", "app.schemas.openai",
"app.schemas.plugin", "app.schemas.plugin",
"app.schemas.query",
"app.schemas.response", "app.schemas.response",
"app.schemas.rule", "app.schemas.rule",
"app.schemas.search", "app.schemas.search",
@@ -9142,6 +9171,7 @@
"app.sdk.media", "app.sdk.media",
"app.sdk.network", "app.sdk.network",
"app.sdk.plugins", "app.sdk.plugins",
"app.sdk.queries",
"app.sdk.security", "app.sdk.security",
"app.sdk.services", "app.sdk.services",
"app.sdk.string", "app.sdk.string",
@@ -10174,6 +10174,168 @@
"target": "app.runtime.extensions.plugin_manager.PluginManager" "target": "app.runtime.extensions.plugin_manager.PluginManager"
} }
], ],
"app.sdk.queries": [
{
"kind": "import",
"name": "DEFAULT_QUERY_PAGE_SIZE",
"target": "app.schemas.query.DEFAULT_QUERY_PAGE_SIZE"
},
{
"kind": "import",
"name": "DownloadHistoryFilter",
"target": "app.schemas.query.DownloadHistoryFilter"
},
{
"kind": "import",
"name": "DownloadHistorySnapshot",
"target": "app.schemas.query.DownloadHistorySnapshot"
},
{
"kind": "import",
"name": "MAX_QUERY_PAGE_SIZE",
"target": "app.schemas.query.MAX_QUERY_PAGE_SIZE"
},
{
"kind": "import",
"name": "MediaIdentityQuery",
"target": "app.schemas.query.MediaIdentityQuery"
},
{
"kind": "import",
"name": "QueryPage",
"target": "app.schemas.query.QueryPage"
},
{
"kind": "import",
"name": "QueryPageRequest",
"target": "app.schemas.query.QueryPageRequest"
},
{
"kind": "import",
"name": "QuerySort",
"target": "app.schemas.query.QuerySort"
},
{
"kind": "import",
"name": "QuerySortDirection",
"target": "app.schemas.query.QuerySortDirection"
},
{
"kind": "import",
"name": "QuerySortField",
"target": "app.schemas.query.QuerySortField"
},
{
"kind": "import",
"name": "SubscriptionFilter",
"target": "app.schemas.query.SubscriptionFilter"
},
{
"kind": "import",
"name": "SubscriptionHistoryFilter",
"target": "app.schemas.query.SubscriptionHistoryFilter"
},
{
"kind": "import",
"name": "SubscriptionHistorySnapshot",
"target": "app.schemas.query.SubscriptionHistorySnapshot"
},
{
"kind": "import",
"name": "SubscriptionSnapshot",
"target": "app.schemas.query.SubscriptionSnapshot"
},
{
"kind": "import",
"name": "TransferHistoryFilter",
"target": "app.schemas.query.TransferHistoryFilter"
},
{
"kind": "import",
"name": "TransferHistorySnapshot",
"target": "app.schemas.query.TransferHistorySnapshot"
},
{
"kind": "AsyncFunctionDef",
"name": "async_get_download_history",
"target": ""
},
{
"kind": "AsyncFunctionDef",
"name": "async_get_subscription",
"target": ""
},
{
"kind": "AsyncFunctionDef",
"name": "async_get_subscription_history",
"target": ""
},
{
"kind": "AsyncFunctionDef",
"name": "async_get_transfer_history",
"target": ""
},
{
"kind": "AsyncFunctionDef",
"name": "async_list_download_history",
"target": ""
},
{
"kind": "AsyncFunctionDef",
"name": "async_list_subscription_history",
"target": ""
},
{
"kind": "AsyncFunctionDef",
"name": "async_list_subscriptions",
"target": ""
},
{
"kind": "AsyncFunctionDef",
"name": "async_list_transfer_history",
"target": ""
},
{
"kind": "FunctionDef",
"name": "get_download_history",
"target": ""
},
{
"kind": "FunctionDef",
"name": "get_subscription",
"target": ""
},
{
"kind": "FunctionDef",
"name": "get_subscription_history",
"target": ""
},
{
"kind": "FunctionDef",
"name": "get_transfer_history",
"target": ""
},
{
"kind": "FunctionDef",
"name": "list_download_history",
"target": ""
},
{
"kind": "FunctionDef",
"name": "list_subscription_history",
"target": ""
},
{
"kind": "FunctionDef",
"name": "list_subscriptions",
"target": ""
},
{
"kind": "FunctionDef",
"name": "list_transfer_history",
"target": ""
}
],
"app.sdk.security": [ "app.sdk.security": [
{ {
"kind": "import", "kind": "import",
+121 -38
View File
@@ -11,6 +11,7 @@ import asyncio
import inspect import inspect
import threading import threading
from collections.abc import Callable from collections.abc import Callable
from types import SimpleNamespace
from typing import Any from typing import Any
import pytest import pytest
@@ -19,26 +20,23 @@ from app.db.models.downloadhistory import DownloadHistory as DownloadHistoryMode
from app.db.models.subscribe import Subscribe as SubscribeModel from app.db.models.subscribe import Subscribe as SubscribeModel
from app.db.models.subscribehistory import SubscribeHistory as SubscribeHistoryModel from app.db.models.subscribehistory import SubscribeHistory as SubscribeHistoryModel
from app.db.models.transferhistory import TransferHistory as TransferHistoryModel from app.db.models.transferhistory import TransferHistory as TransferHistoryModel
from app.schemas.history import (
DownloadHistory as DownloadHistoryDTO,
TransferHistory as TransferHistoryDTO,
)
from app.schemas.query import ( from app.schemas.query import (
DownloadHistoryFilter, DownloadHistoryFilter,
DownloadHistorySnapshot,
QueryPage, QueryPage,
QueryPageRequest, QueryPageRequest,
QuerySort, QuerySort,
QuerySortDirection, QuerySortDirection,
QuerySortField, QuerySortField,
SubscribeHistory as SubscribeHistoryDTO,
SubscriptionFilter, SubscriptionFilter,
SubscriptionHistoryFilter, SubscriptionHistoryFilter,
SubscriptionHistorySnapshot,
SubscriptionSnapshot,
TransferHistoryFilter, TransferHistoryFilter,
TransferHistorySnapshot,
) )
from app.schemas.subscribe import Subscribe as SubscribeDTO
from app.schemas.types import MediaSource, MediaType from app.schemas.types import MediaSource, MediaType
TMDB = MediaSource.TMDB.value TMDB = MediaSource.TMDB.value
@@ -95,6 +93,7 @@ def _subscribe(
episode_group: str | None = None, episode_group: str | None = None,
date: str = "2026-08-27 10:00:00", date: str = "2026-08-27 10:00:00",
music_type: str | None = None, music_type: str | None = None,
manual_total_episode: int | None = 0,
) -> SubscribeModel: ) -> SubscribeModel:
"""构造隔离用例使用的订阅行。""" """构造隔离用例使用的订阅行。"""
return SubscribeModel( return SubscribeModel(
@@ -108,6 +107,7 @@ def _subscribe(
username=username, username=username,
date=date, date=date,
music_type=music_type, music_type=music_type,
manual_total_episode=manual_total_episode,
) )
@@ -218,13 +218,15 @@ def _assert_projected_page(page: QueryPage, dto_type: type[Any], model_type: typ
def test_sdk_projects_subscription_and_three_history_domains_to_dtos(db, query_sdk): def test_sdk_projects_subscription_and_three_history_domains_to_dtos(db, query_sdk):
"""订阅、订阅完成历史、下载历史、整理历史均只返回 Pydantic 投影。""" """订阅、订阅完成历史、下载历史、整理历史均只返回 Pydantic 投影。"""
sdk, _executor = query_sdk sdk, _executor = query_sdk
subscribe = db.add(_subscribe("订阅 DTO", media_id="dto-sub")) subscribe = db.add(
subscribe_history = db.add( _subscribe(
_subscribe_history("订阅历史 DTO", media_id="dto-sub-history") "订阅 DTO",
) media_id="dto-sub",
download_history = db.add( manual_total_episode=1,
_download_history("下载历史 DTO", media_id="dto-download", path="/dto/download") )
) )
subscribe_history = db.add(_subscribe_history("订阅历史 DTO", media_id="dto-sub-history"))
download_history = db.add(_download_history("下载历史 DTO", media_id="dto-download", path="/dto/download"))
transfer_history = db.add( transfer_history = db.add(
_transfer_history( _transfer_history(
"整理历史 DTO", "整理历史 DTO",
@@ -235,13 +237,12 @@ def test_sdk_projects_subscription_and_three_history_domains_to_dtos(db, query_s
) )
_assert_projected_page( _assert_projected_page(
sdk.list_subscriptions( subscription_page := sdk.list_subscriptions(SubscriptionFilter(media_source=TMDB, media_id="dto-sub")),
SubscriptionFilter(media_source=TMDB, media_id="dto-sub") SubscriptionSnapshot,
),
SubscribeDTO,
SubscribeModel, SubscribeModel,
subscribe.id, subscribe.id,
) )
assert subscription_page.items[0].manual_total_episode == 1
_assert_projected_page( _assert_projected_page(
sdk.list_subscription_history( sdk.list_subscription_history(
SubscriptionHistoryFilter( SubscriptionHistoryFilter(
@@ -249,23 +250,19 @@ def test_sdk_projects_subscription_and_three_history_domains_to_dtos(db, query_s
media_id="dto-sub-history", media_id="dto-sub-history",
) )
), ),
SubscribeHistoryDTO, SubscriptionHistorySnapshot,
SubscribeHistoryModel, SubscribeHistoryModel,
subscribe_history.id, subscribe_history.id,
) )
_assert_projected_page( _assert_projected_page(
sdk.list_download_history( sdk.list_download_history(DownloadHistoryFilter(media_source=TMDB, media_id="dto-download")),
DownloadHistoryFilter(media_source=TMDB, media_id="dto-download") DownloadHistorySnapshot,
),
DownloadHistoryDTO,
DownloadHistoryModel, DownloadHistoryModel,
download_history.id, download_history.id,
) )
_assert_projected_page( _assert_projected_page(
sdk.list_transfer_history( sdk.list_transfer_history(TransferHistoryFilter(media_source=TMDB, media_id="dto-transfer")),
TransferHistoryFilter(media_source=TMDB, media_id="dto-transfer") TransferHistorySnapshot,
),
TransferHistoryDTO,
TransferHistoryModel, TransferHistoryModel,
transfer_history.id, transfer_history.id,
) )
@@ -422,11 +419,12 @@ def test_sdk_applies_combined_filters_in_each_query_domain(db, query_sdk):
media_source=TMDB, media_source=TMDB,
media_id="combo-download", media_id="combo-download",
media_types=(MediaType.TV,), media_types=(MediaType.TV,),
title="Combo", title="Combo Film",
text="match",
year="2026", year="2026",
seasons="S02", seasons="S02",
episodes="E03", episodes="E03",
path="match", path="/combo/match.mkv",
download_hash="combo-hash", download_hash="combo-hash",
username="alice", username="alice",
episode_group="eg-a", episode_group="eg-a",
@@ -475,7 +473,7 @@ def test_sdk_applies_combined_filters_in_each_query_domain(db, query_sdk):
media_types=(MediaType.TV,), media_types=(MediaType.TV,),
media_sources=(MediaSource.TMDB,), media_sources=(MediaSource.TMDB,),
require_media_identity=True, require_media_identity=True,
title="Transfer", title="Transfer Combo",
text="dest-match", text="dest-match",
year="2026", year="2026",
seasons="S02", seasons="S02",
@@ -533,9 +531,7 @@ def test_sdk_pagination_reports_total_and_stable_date_id_order(db, query_sdk):
assert page_one.page == 1 assert page_one.page == 1
assert page_one.count == 2 assert page_one.count == 2
assert page_one.has_next is True assert page_one.has_next is True
assert [item.id for item in page_one.items] == [ assert [item.id for item in page_one.items] == [row.id for row in expected_desc[:2]]
row.id for row in expected_desc[:2]
]
assert page_two.total == 3 assert page_two.total == 3
assert page_two.has_next is False assert page_two.has_next is False
assert [item.id for item in page_two.items] == [expected_desc[2].id] assert [item.id for item in page_two.items] == [expected_desc[2].id]
@@ -552,9 +548,98 @@ def test_sdk_pagination_reports_total_and_stable_date_id_order(db, query_sdk):
), ),
) )
expected_asc = sorted(rows, key=lambda row: (row.date, row.id)) expected_asc = sorted(rows, key=lambda row: (row.date, row.id))
assert [item.id for item in asc_page.items] == [ assert [item.id for item in asc_page.items] == [row.id for row in expected_asc]
row.id for row in expected_asc
]
def test_structured_text_fields_are_exact_and_text_search_is_explicit(db, query_sdk):
"""结构化字段保持精确匹配,只有 text 承担转义后的模糊搜索。"""
sdk, _executor = query_sdk
db.add(
_download_history(
"Exact Film Extended",
media_id="exact-download",
path="/exact/extended.mkv",
),
_download_history(
"Exact Film",
media_id="exact-download",
path="/exact/base.mkv",
),
_transfer_history(
"Exact Transfer Extended",
media_id="exact-transfer",
src="/exact/extended-src.mkv",
dest="/exact/extended-dest.mkv",
),
_transfer_history(
"Exact Transfer",
media_id="exact-transfer",
src="/exact/base-src.mkv",
dest="/exact/base-dest.mkv",
),
)
exact_downloads = sdk.list_download_history(
DownloadHistoryFilter(
media_source=TMDB,
media_id="exact-download",
title="Exact Film",
)
)
assert [item.path for item in exact_downloads.items] == ["/exact/base.mkv"]
fuzzy_downloads = sdk.list_download_history(
DownloadHistoryFilter(
media_source=TMDB,
media_id="exact-download",
text="extended",
)
)
assert [item.path for item in fuzzy_downloads.items] == ["/exact/extended.mkv"]
exact_transfers = sdk.list_transfer_history(
TransferHistoryFilter(
media_source=TMDB,
media_id="exact-transfer",
title="Exact Transfer",
)
)
assert [item.src for item in exact_transfers.items] == ["/exact/base-src.mkv"]
fuzzy_transfers = sdk.list_transfer_history(
TransferHistoryFilter(
media_source=TMDB,
media_id="exact-transfer",
text="extended-dest",
)
)
assert [item.src for item in fuzzy_transfers.items] == ["/exact/extended-src.mkv"]
def test_snapshots_normalize_legacy_identity_and_transfer_status():
"""旧半对身份和 NULL 整理状态不得使分页投影失败或产生假成功。"""
dirty_transfer = SimpleNamespace(
id=1,
media_source=TMDB,
media_id=" ",
status=None,
)
snapshot = TransferHistorySnapshot.model_validate(dirty_transfer)
assert snapshot.media_source is None
assert snapshot.media_id is None
assert snapshot.status is False
def test_query_snapshots_are_owned_by_the_sdk_contract_module():
"""公开查询返回值由独立快照定义,不复用宿主写入或 API 响应模型。"""
snapshots = (
SubscriptionSnapshot,
SubscriptionHistorySnapshot,
DownloadHistorySnapshot,
TransferHistorySnapshot,
)
assert all(snapshot.__module__ == "app.schemas.query" for snapshot in snapshots)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -590,9 +675,7 @@ def test_sdk_get_returns_none_for_missing_records(query_sdk):
def test_sdk_sync_async_semantics_match_and_async_uses_executor(db, query_sdk): def test_sdk_sync_async_semantics_match_and_async_uses_executor(db, query_sdk):
"""四个查询门面的异步结果与同步一致,并交给 executor 线程。""" """四个查询门面的异步结果与同步一致,并交给 executor 线程。"""
sdk, executor = query_sdk sdk, executor = query_sdk
subscribe = db.add( subscribe = db.add(_subscribe("订阅同步异步", media_id="async-subscription"))
_subscribe("订阅同步异步", media_id="async-subscription")
)
subscribe_history = db.add( subscribe_history = db.add(
_subscribe_history( _subscribe_history(
"订阅历史同步异步", "订阅历史同步异步",