refactor: normalize downloader file contracts

This commit is contained in:
jxxghp
2026-08-24 02:10:04 +08:00
parent 83318b12c0
commit 52fd495d38
12 changed files with 123 additions and 32 deletions
+3 -3
View File
@@ -21,7 +21,7 @@ from app.runtime.log import logger
from app.schemas.exception import RateLimitExceededException
from app.schemas.transfer import TransferInfo
from app.schemas.mediaserver import ExistMediaInfo
from app.schemas.transfer import DownloaderTorrent
from app.schemas.transfer import DownloaderFile, DownloaderTorrent
from app.schemas.message import IncomingMessage
from app.schemas.mediaserver import WebhookEventInfo
from app.schemas.tmdb import TmdbEpisode
@@ -1023,12 +1023,12 @@ class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin,
def torrent_files(
self, tid: str, downloader: Optional[str] = None
) -> Optional[Any]:
) -> Optional[List[DownloaderFile]]:
"""
获取种子文件
:param tid: 种子Hash
:param downloader: 下载器
:return: 种子文件,具体类型由下载器实现决定(链层不引入下载器协议类型)
:return: 与下载器 SDK 解耦的统一文件项列表
"""
return self.run_module("torrent_files", tid=tid, downloader=downloader)
+15 -1
View File
@@ -5,7 +5,8 @@
(任务添加、原始状态映射、任务列表构建)仍留在各模块。
"""
from pathlib import Path
from typing import Optional, Tuple, Union
from collections.abc import Callable
from typing import Any, List, Optional, Tuple, TypeVar, Union
from torrentool.torrent import Torrent
@@ -16,6 +17,9 @@ from app.runtime.log import logger
from app.schemas.types import TorrentQueryStatus, TorrentStatus
TFile = TypeVar("TFile")
class _DownloaderModuleBase(_ModuleBase, _DownloaderBase[TService]):
"""
下载器模块业务样板基类。
@@ -43,6 +47,16 @@ class _DownloaderModuleBase(_ModuleBase, _DownloaderBase[TService]):
logger.info(f"{self.get_name()}下载器 {name} 连接断开,尝试重连 ...")
server.reconnect()
@staticmethod
def _normalize_torrent_files(
files: Any, item_factory: Callable[[Any], TFile]
) -> Optional[List[TFile]]:
"""把 provider 文件集合统一投影为不依赖外部 SDK 的宿主 DTO。"""
if files is None:
return None
source = getattr(files, "data", files)
return [item_factory(item) for item in source]
def _get_torrent_info(self, content: Union[Path, str, bytes]) \
-> Tuple[Optional[Torrent], Optional[bytes]]:
"""
+8 -6
View File
@@ -1,15 +1,13 @@
from pathlib import Path
from typing import Set, Tuple, Optional, Union, List, Dict
from qbittorrentapi import TorrentFilesList
from app.schemas.dashboard import DownloaderInfo as _SchemaDownloaderInfo
from app.domain.metainfo import MetaInfo
from app.runtime.log import logger
from app.runtime.settings import RuntimeSettingsCompat
from app.modules._base import _DownloaderModuleBase
from app.modules.qbittorrent.qbittorrent import Qbittorrent
from app.schemas.transfer import DownloaderTorrent
from app.schemas.transfer import DownloaderFile, DownloaderTorrent
from app.schemas.types import (
DownloadTaskState,
DownloaderType,
@@ -515,14 +513,18 @@ class QbittorrentModule(_DownloaderModuleBase[Qbittorrent]):
return None
return server.stop_torrents(ids=hashs)
def torrent_files(self, tid: str, downloader: Optional[str] = None) -> Optional[TorrentFilesList]:
def torrent_files(
self, tid: str, downloader: Optional[str] = None
) -> Optional[List[DownloaderFile]]:
"""
获取种子文件列表
获取种子文件列表,并在模块边界隔离 qBittorrent SDK 集合类型。
"""
server: Qbittorrent = self.get_instance(downloader)
if not server:
return None
return server.get_files(tid=tid)
return self._normalize_torrent_files(
server.get_files(tid=tid), DownloaderFile.model_validate
)
def downloader_info(self, downloader: Optional[str] = None) -> Optional[List[_SchemaDownloaderInfo]]:
"""
+6 -4
View File
@@ -7,7 +7,7 @@ from app.runtime.log import logger
from app.runtime.settings import RuntimeSettingsCompat
from app.modules._base import _DownloaderModuleBase
from app.modules.rtorrent.rtorrent import Rtorrent
from app.schemas.transfer import DownloaderTorrent
from app.schemas.transfer import DownloaderFile, DownloaderTorrent
from app.schemas.types import (
DownloadTaskState,
DownloaderType,
@@ -526,14 +526,16 @@ class RtorrentModule(_DownloaderModuleBase[Rtorrent]):
def torrent_files(
self, tid: str, downloader: Optional[str] = None
) -> Optional[List[Dict]]:
) -> Optional[List[DownloaderFile]]:
"""
获取种子文件列表
获取种子文件列表,并在模块边界把字典投影为宿主 DTO。
"""
server: Rtorrent = self.get_instance(downloader)
if not server:
return None
return server.get_files(tid=tid)
return self._normalize_torrent_files(
server.get_files(tid=tid), DownloaderFile.model_validate
)
def downloader_info(
self, downloader: Optional[str] = None
+8 -6
View File
@@ -1,15 +1,13 @@
from pathlib import Path
from typing import Set, Tuple, Optional, Union, List, Dict
from transmission_rpc import File
from app.schemas.dashboard import DownloaderInfo as _SchemaDownloaderInfo
from app.domain.metainfo import MetaInfo
from app.runtime.log import logger
from app.runtime.settings import RuntimeSettingsCompat
from app.modules._base import _DownloaderModuleBase
from app.modules.transmission.transmission import Transmission
from app.schemas.transfer import DownloaderTorrent
from app.schemas.transfer import DownloaderFile, DownloaderTorrent
from app.schemas.types import (
DownloadTaskState,
DownloaderType,
@@ -530,15 +528,19 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
return None
return server.stop_torrents(ids=hashs)
def torrent_files(self, tid: str, downloader: Optional[str] = None) -> Optional[List[File]]:
def torrent_files(
self, tid: str, downloader: Optional[str] = None
) -> Optional[List[DownloaderFile]]:
"""
获取种子文件列表
获取种子文件列表,并在模块边界隔离 Transmission SDK 对象。
"""
# 获取下载器
server: Transmission = self.get_instance(downloader)
if not server:
return None
return server.get_files(tid=tid)
return self._normalize_torrent_files(
server.get_files(tid=tid), DownloaderFile.model_validate
)
def downloader_info(self, downloader: Optional[str] = None) -> Optional[List[_SchemaDownloaderInfo]]:
"""
+1 -1
View File
@@ -225,7 +225,7 @@ _METHOD_CONTRACTS = {
"list_torrents": ModuleMethodContract(family="downloader", input_contract="TorrentListRequest", result_contract="list[DownloaderTorrent]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("status", "hashs", "downloader", "include_all_tags")),
"filter_torrents": ModuleMethodContract(family="downloader", input_contract="TorrentFilterRequest", result_contract="list[TorrentInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("rule_groups", "torrent_list", "mediainfo")),
"refresh_torrents": ModuleMethodContract(family="downloader", input_contract="TorrentRefreshRequest", result_contract="list[TorrentInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("site", "keyword", "cat", "page", "mtype")),
"torrent_files": ModuleMethodContract(family="downloader", input_contract="TorrentFilesRequest", result_contract="DownloaderFileCollection | None", required_parameters=("tid", "downloader")),
"torrent_files": ModuleMethodContract(family="downloader", input_contract="TorrentFilesRequest", result_contract="list[DownloaderFile]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("tid", "downloader")),
"get_torrent_trackers": ModuleMethodContract(family="downloader", input_contract="TorrentTrackersRequest", result_contract="dict[str, list[str]] | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.ORDERED_MAPPING_MERGE, required_parameters=("hash_string", "downloader")),
"download": ModuleMethodContract(family="downloader", input_contract="DownloadTaskRequest", result_contract="DownloadTaskResult | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("content", "download_dir", "cookie", "episodes", "category", "label", "downloader")),
"download_added": ModuleMethodContract(family="downloader", input_contract="DownloadAddedHook", result_contract="None", aggregation=ModuleResultAggregation.FAN_OUT, required_parameters=("context", "torrent_content", "download_dir"), plugin_short_circuit=False),
+13 -1
View File
@@ -1,7 +1,7 @@
from pathlib import Path
from typing import List, Optional, Union
from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field
from app.schemas.media import OptionalMediaIdentityMixin
from app.schemas.types import MediaSource, MusicTargetEntityType
@@ -43,6 +43,18 @@ class DownloaderTorrent(BaseModel):
left_time: Optional[str] = None
class DownloaderFile(BaseModel):
"""下载器文件项的宿主投影,隔离各 provider SDK 的对象差异。"""
model_config = ConfigDict(from_attributes=True)
id: Optional[Union[int, str]] = None
name: str
size: Optional[int] = None
priority: Optional[int] = None
progress: Optional[float] = None
class DownloadTaskMedia(OptionalMediaIdentityMixin, BaseModel):
"""下载任务关联的影视或音乐媒体摘要。"""