mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
refactor(sdk): isolate plugin query snapshots
This commit is contained in:
+33
-3
@@ -1441,8 +1441,8 @@
|
||||
"runtime_only": true
|
||||
}
|
||||
},
|
||||
"edge_count": 6898,
|
||||
"edge_sha256": "c73faf7e29ee12a862cf2332f1803fd4eb8a3f4d2994ed81075ff198e896f8df",
|
||||
"edge_count": 6924,
|
||||
"edge_sha256": "d5471439086bcfdd8060b77670af6f821a34e3a864ba7e2cb7230c599a1cb3bb",
|
||||
"edges": [
|
||||
"app -> app.runtime",
|
||||
"app -> app.runtime.compat",
|
||||
@@ -4008,6 +4008,10 @@
|
||||
"app.application.configuration -> app.schemas.types",
|
||||
"app.application.dashboard -> app.schemas",
|
||||
"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.system",
|
||||
"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.transfersettlementreceipt",
|
||||
"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.oper",
|
||||
"app.db.adapters.download -> app.db.oper.downloadfailure",
|
||||
@@ -7629,6 +7645,10 @@
|
||||
"app.schemas.openai -> app.schemas.common",
|
||||
"app.schemas.plugin -> app.schemas",
|
||||
"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.localization",
|
||||
"app.schemas.search -> app.schemas",
|
||||
@@ -7748,6 +7768,10 @@
|
||||
"app.sdk.plugins -> app.runtime.extensions",
|
||||
"app.sdk.plugins -> app.runtime.extensions.module_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.web",
|
||||
"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.events",
|
||||
"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.history",
|
||||
"app.startup.initializers.modules -> app.application.image",
|
||||
@@ -7971,6 +7996,7 @@
|
||||
"app.startup.initializers.modules -> app.db",
|
||||
"app.startup.initializers.modules -> app.db.adapters",
|
||||
"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.outbox",
|
||||
"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.actions"
|
||||
],
|
||||
"module_count": 844,
|
||||
"module_count": 848,
|
||||
"modules": [
|
||||
"app",
|
||||
"app.adapters",
|
||||
@@ -8603,6 +8629,7 @@
|
||||
"app.application.commands",
|
||||
"app.application.configuration",
|
||||
"app.application.dashboard",
|
||||
"app.application.data_query",
|
||||
"app.application.database",
|
||||
"app.application.directory",
|
||||
"app.application.download",
|
||||
@@ -8735,6 +8762,7 @@
|
||||
"app.db",
|
||||
"app.db.adapters",
|
||||
"app.db.adapters.chain",
|
||||
"app.db.adapters.data_query",
|
||||
"app.db.adapters.download",
|
||||
"app.db.adapters.outbox",
|
||||
"app.db.adapters.pluginidentity",
|
||||
@@ -9111,6 +9139,7 @@
|
||||
"app.schemas.notification",
|
||||
"app.schemas.openai",
|
||||
"app.schemas.plugin",
|
||||
"app.schemas.query",
|
||||
"app.schemas.response",
|
||||
"app.schemas.rule",
|
||||
"app.schemas.search",
|
||||
@@ -9142,6 +9171,7 @@
|
||||
"app.sdk.media",
|
||||
"app.sdk.network",
|
||||
"app.sdk.plugins",
|
||||
"app.sdk.queries",
|
||||
"app.sdk.security",
|
||||
"app.sdk.services",
|
||||
"app.sdk.string",
|
||||
|
||||
@@ -10174,6 +10174,168 @@
|
||||
"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": [
|
||||
{
|
||||
"kind": "import",
|
||||
|
||||
+121
-38
@@ -11,6 +11,7 @@ import asyncio
|
||||
import inspect
|
||||
import threading
|
||||
from collections.abc import Callable
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
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.subscribehistory import SubscribeHistory as SubscribeHistoryModel
|
||||
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 (
|
||||
DownloadHistoryFilter,
|
||||
DownloadHistorySnapshot,
|
||||
QueryPage,
|
||||
QueryPageRequest,
|
||||
QuerySort,
|
||||
QuerySortDirection,
|
||||
QuerySortField,
|
||||
SubscribeHistory as SubscribeHistoryDTO,
|
||||
SubscriptionFilter,
|
||||
SubscriptionHistoryFilter,
|
||||
SubscriptionHistorySnapshot,
|
||||
SubscriptionSnapshot,
|
||||
TransferHistoryFilter,
|
||||
TransferHistorySnapshot,
|
||||
)
|
||||
from app.schemas.subscribe import Subscribe as SubscribeDTO
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
|
||||
|
||||
TMDB = MediaSource.TMDB.value
|
||||
|
||||
|
||||
@@ -95,6 +93,7 @@ def _subscribe(
|
||||
episode_group: str | None = None,
|
||||
date: str = "2026-08-27 10:00:00",
|
||||
music_type: str | None = None,
|
||||
manual_total_episode: int | None = 0,
|
||||
) -> SubscribeModel:
|
||||
"""构造隔离用例使用的订阅行。"""
|
||||
return SubscribeModel(
|
||||
@@ -108,6 +107,7 @@ def _subscribe(
|
||||
username=username,
|
||||
date=date,
|
||||
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):
|
||||
"""订阅、订阅完成历史、下载历史、整理历史均只返回 Pydantic 投影。"""
|
||||
sdk, _executor = query_sdk
|
||||
subscribe = db.add(_subscribe("订阅 DTO", media_id="dto-sub"))
|
||||
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")
|
||||
subscribe = db.add(
|
||||
_subscribe(
|
||||
"订阅 DTO",
|
||||
media_id="dto-sub",
|
||||
manual_total_episode=1,
|
||||
)
|
||||
)
|
||||
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(
|
||||
"整理历史 DTO",
|
||||
@@ -235,13 +237,12 @@ def test_sdk_projects_subscription_and_three_history_domains_to_dtos(db, query_s
|
||||
)
|
||||
|
||||
_assert_projected_page(
|
||||
sdk.list_subscriptions(
|
||||
SubscriptionFilter(media_source=TMDB, media_id="dto-sub")
|
||||
),
|
||||
SubscribeDTO,
|
||||
subscription_page := sdk.list_subscriptions(SubscriptionFilter(media_source=TMDB, media_id="dto-sub")),
|
||||
SubscriptionSnapshot,
|
||||
SubscribeModel,
|
||||
subscribe.id,
|
||||
)
|
||||
assert subscription_page.items[0].manual_total_episode == 1
|
||||
_assert_projected_page(
|
||||
sdk.list_subscription_history(
|
||||
SubscriptionHistoryFilter(
|
||||
@@ -249,23 +250,19 @@ def test_sdk_projects_subscription_and_three_history_domains_to_dtos(db, query_s
|
||||
media_id="dto-sub-history",
|
||||
)
|
||||
),
|
||||
SubscribeHistoryDTO,
|
||||
SubscriptionHistorySnapshot,
|
||||
SubscribeHistoryModel,
|
||||
subscribe_history.id,
|
||||
)
|
||||
_assert_projected_page(
|
||||
sdk.list_download_history(
|
||||
DownloadHistoryFilter(media_source=TMDB, media_id="dto-download")
|
||||
),
|
||||
DownloadHistoryDTO,
|
||||
sdk.list_download_history(DownloadHistoryFilter(media_source=TMDB, media_id="dto-download")),
|
||||
DownloadHistorySnapshot,
|
||||
DownloadHistoryModel,
|
||||
download_history.id,
|
||||
)
|
||||
_assert_projected_page(
|
||||
sdk.list_transfer_history(
|
||||
TransferHistoryFilter(media_source=TMDB, media_id="dto-transfer")
|
||||
),
|
||||
TransferHistoryDTO,
|
||||
sdk.list_transfer_history(TransferHistoryFilter(media_source=TMDB, media_id="dto-transfer")),
|
||||
TransferHistorySnapshot,
|
||||
TransferHistoryModel,
|
||||
transfer_history.id,
|
||||
)
|
||||
@@ -422,11 +419,12 @@ def test_sdk_applies_combined_filters_in_each_query_domain(db, query_sdk):
|
||||
media_source=TMDB,
|
||||
media_id="combo-download",
|
||||
media_types=(MediaType.TV,),
|
||||
title="Combo",
|
||||
title="Combo Film",
|
||||
text="match",
|
||||
year="2026",
|
||||
seasons="S02",
|
||||
episodes="E03",
|
||||
path="match",
|
||||
path="/combo/match.mkv",
|
||||
download_hash="combo-hash",
|
||||
username="alice",
|
||||
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_sources=(MediaSource.TMDB,),
|
||||
require_media_identity=True,
|
||||
title="Transfer",
|
||||
title="Transfer Combo",
|
||||
text="dest-match",
|
||||
year="2026",
|
||||
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.count == 2
|
||||
assert page_one.has_next is True
|
||||
assert [item.id for item in page_one.items] == [
|
||||
row.id for row in expected_desc[:2]
|
||||
]
|
||||
assert [item.id for item in page_one.items] == [row.id for row in expected_desc[:2]]
|
||||
assert page_two.total == 3
|
||||
assert page_two.has_next is False
|
||||
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))
|
||||
assert [item.id for item in asc_page.items] == [
|
||||
row.id for row in expected_asc
|
||||
]
|
||||
assert [item.id for item in asc_page.items] == [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(
|
||||
@@ -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):
|
||||
"""四个查询门面的异步结果与同步一致,并交给 executor 线程。"""
|
||||
sdk, executor = query_sdk
|
||||
subscribe = db.add(
|
||||
_subscribe("订阅同步异步", media_id="async-subscription")
|
||||
)
|
||||
subscribe = db.add(_subscribe("订阅同步异步", media_id="async-subscription"))
|
||||
subscribe_history = db.add(
|
||||
_subscribe_history(
|
||||
"订阅历史同步异步",
|
||||
|
||||
Reference in New Issue
Block a user