fix(types): govern Transmission module SDK projections

This commit is contained in:
jxxghp
2026-10-17 06:29:15 +08:00
parent 68101746ae
commit 79d5e5aa71

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Literal, Optional, Set, Tuple, Union, overload
from app.domain import torrent as torrent_rules
from app.domain.metainfo import MetaInfo
@@ -19,6 +21,17 @@ from app.schemas.types import (
TorrentStatus,
)
if TYPE_CHECKING:
from transmission_rpc import Torrent
# 只描述 SDK 已公开的数值字段及旧版别名;不转换原始返回值。
_IntTorrentAttribute = Literal[
"total_size", "totalSize", "rate_download", "rateDownload", "rate_upload", "rateUpload",
"left_until_done", "leftUntilDone", "seed_idle_limit", "seedIdleLimit",
"download_limit", "downloadLimit", "upload_limit", "uploadLimit",
]
_FloatTorrentAttribute = Literal["progress", "percent_done", "seed_ratio_limit", "seedRatioLimit"]
_TRANSMISSION_DOWNLOADING_STATES = {
"download pending",
"downloading",
@@ -28,8 +41,33 @@ _TRANSMISSION_PAUSED_STATES = {
}
@overload
def _get_torrent_attr(torrent_data: Torrent, *attr_names: _IntTorrentAttribute) -> Optional[int]:
"""读取 SDK 整数计数或限额,字段均不可用时返回空值。"""
...
@overload
def _get_torrent_attr(torrent_data: Torrent, *attr_names: _FloatTorrentAttribute) -> Optional[float]:
"""读取 SDK 比例或进度,字段均不可用时返回空值。"""
...
def _get_torrent_attr(
torrent_data: Torrent, *attr_names: Union[_IntTorrentAttribute, _FloatTorrentAttribute],
) -> Optional[Union[int, float]]:
"""
兼容 transmission-rpc 新旧字段名。
"""
for attr_name in attr_names:
try:
value: Optional[Union[int, float]] = getattr(torrent_data, attr_name)
return value
except (AttributeError, KeyError, TypeError, ValueError):
continue
return None
def _normalize_transmission_progress(
torrent_data: Any, attr_getter: Callable[..., Any]
torrent_data: Torrent, attr_getter: Callable[[Torrent, _FloatTorrentAttribute], Optional[float]]
) -> float:
"""读取 Transmission 任务进度并统一为百分比。"""
progress = attr_getter(torrent_data, "progress")
@@ -80,16 +118,16 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
"""
return 2
def stop(self):
def stop(self) -> None:
"""下载器客户端由服务基类管理,本模块无额外停止动作。"""
pass
def init_setting(self) -> Tuple[str, Union[str, bool]]:
def init_setting(self) -> Optional[Tuple[str, Union[str, bool]]]:
"""下载器实例由系统配置管理,不声明独立模块开关。"""
pass
def download(self, content: Union[Path, str, bytes], download_dir: Path, cookie: str,
episodes: Set[int] = None, category: Optional[str] = None, label: Optional[str] = None,
episodes: Optional[Set[int]] = None, category: Optional[str] = None, label: Optional[str] = None,
downloader: Optional[str] = None,
custom_words: Optional[str] = None) -> Optional[Tuple[Optional[str], Optional[str], Optional[str], str]]:
"""
@@ -109,20 +147,20 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
return None, None, None, "下载内容为空"
# 读取种子的名称
torrent_from_file, content = self._get_torrent_info(content)
expected_hashes = torrent_rules.download_hashes(content) if isinstance(content, (str, bytes)) else frozenset()
torrent_from_file, torrent_content = self._get_torrent_info(content)
expected_hashes = torrent_rules.download_hashes(torrent_content) if isinstance(torrent_content, (str, bytes)) else frozenset()
# 检查是否为磁力链接
is_magnet = isinstance(content, str) and content.startswith("magnet:") or isinstance(content,
bytes) and content.startswith(
is_magnet = isinstance(torrent_content, str) and torrent_content.startswith("magnet:") or isinstance(torrent_content,
bytes) and torrent_content.startswith(
b"magnet:")
if not torrent_from_file and not is_magnet:
if not torrent_content or (not torrent_from_file and not is_magnet):
return None, None, None, "添加种子任务失败:无法读取种子文件"
# 失败提示使用种子名称(磁力链接保留原文),不拼接种子文件的二进制内容
torrent_desc = torrent_from_file.name if torrent_from_file else content
torrent_desc = str(torrent_from_file.name if torrent_from_file else torrent_content)
# 获取下载器
server: Transmission = self.get_instance(downloader)
server = self.get_instance(downloader)
if not server:
return None
@@ -138,7 +176,7 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
labels = None
# 添加任务
added_torrent = server.add_torrent(
content=content,
content=torrent_content,
download_dir=self.normalize_path(download_dir, downloader),
is_paused=is_paused,
labels=labels,
@@ -232,8 +270,8 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
return False, f"选集失败,任务 {torrent_hash} 启动任务未确认,请在下载器核实,未记录成功"
return True, "添加下载任务成功"
def list_torrents(self, status: TorrentStatus = None,
hashs: Union[list, str] = None,
def list_torrents(self, status: Optional[Union[TorrentStatus, TorrentQueryStatus, str]] = None,
hashs: Optional[Union[list[str], str]] = None,
downloader: Optional[str] = None,
include_all_tags: bool = False,
) -> Optional[List[DownloaderTorrent]]:
@@ -246,63 +284,54 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
:return: 下载器中符合状态的种子列表
"""
# 获取下载器
servers: Dict[Optional[str], Transmission]
if downloader:
server: Transmission = self.get_instance(downloader)
server = self.get_instance(downloader)
if not server:
return None
servers = {downloader: server}
else:
servers: Dict[str, Transmission] = self.get_instances()
servers = self.get_instances()
ret_torrents = []
query_status = self._normalize_query_status(status)
query_tags = None if include_all_tags else get_runtime_setting('TORRENT_TAG')
def __get_torrent_attr(torrent_data, *attr_names):
"""
兼容 transmission-rpc 新旧字段名。
"""
for attr_name in attr_names:
try:
return getattr(torrent_data, attr_name)
except (AttributeError, KeyError, TypeError, ValueError):
continue
return None
def __get_torrent_size(torrent_data) -> int:
def __get_torrent_size(torrent_data: Torrent) -> int:
"""
获取任务大小。
"""
return __get_torrent_attr(torrent_data, "total_size", "totalSize") or 0
return _get_torrent_attr(torrent_data, "total_size", "totalSize") or 0
def __get_torrent_labels(torrent_data) -> str:
def __get_torrent_labels(torrent_data: Torrent) -> str:
"""
获取任务标签。
"""
return ",".join(getattr(torrent_data, "labels", None) or [])
def __get_torrent_path(torrent_data) -> Path:
def __get_torrent_path(torrent_data: Torrent) -> Path:
"""
获取任务内容路径。
"""
return Path(torrent_data.download_dir) / torrent_data.name
torrent_path: Path = Path(torrent_data.download_dir) / torrent_data.name
return torrent_path
def __build_torrent(downloader_name: str, torrent_data) -> DownloaderTorrent:
def __build_torrent(downloader_name: Optional[str], torrent_data: Torrent) -> DownloaderTorrent:
"""
构造统一下载器任务对象。
"""
meta = MetaInfo(torrent_data.name)
dlspeed = __get_torrent_attr(
dlspeed = _get_torrent_attr(
torrent_data, "rate_download", "rateDownload"
) or 0
upspeed = __get_torrent_attr(
upspeed = _get_torrent_attr(
torrent_data, "rate_upload", "rateUpload"
) or 0
left_until_done = __get_torrent_attr(
left_until_done = _get_torrent_attr(
torrent_data, "left_until_done", "leftUntilDone"
) or 0
torrent_path = __get_torrent_path(torrent_data)
ratio_limit = __get_torrent_attr(torrent_data, "seed_ratio_limit", "seedRatioLimit")
seeding_time_limit = __get_torrent_attr(torrent_data, "seed_idle_limit", "seedIdleLimit")
ratio_limit = _get_torrent_attr(torrent_data, "seed_ratio_limit", "seedRatioLimit")
seeding_time_limit = _get_torrent_attr(torrent_data, "seed_idle_limit", "seedIdleLimit")
return DownloaderTorrent(
downloader=downloader_name,
hash=torrent_data.hashString,
@@ -315,14 +344,14 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
Path(torrent_data.download_dir), downloader_name
) if getattr(torrent_data, "download_dir", None) else None,
content_path=self.normalize_return_path(torrent_path, downloader_name),
progress=_normalize_transmission_progress(torrent_data, __get_torrent_attr),
progress=_normalize_transmission_progress(torrent_data, _get_torrent_attr),
size=__get_torrent_size(torrent_data),
state=self.__normalize_torrent_state(torrent_data.status),
dlspeed=size_tools.format_compact_size(dlspeed),
upspeed=size_tools.format_compact_size(upspeed),
tags=__get_torrent_labels(torrent_data),
download_limit=__get_torrent_attr(torrent_data, "download_limit", "downloadLimit"),
upload_limit=__get_torrent_attr(torrent_data, "upload_limit", "uploadLimit"),
download_limit=_get_torrent_attr(torrent_data, "download_limit", "downloadLimit"),
upload_limit=_get_torrent_attr(torrent_data, "upload_limit", "uploadLimit"),
ratio_limit=ratio_limit,
seeding_time_limit=seeding_time_limit,
left_time=time_tools.format_duration(
@@ -412,7 +441,7 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
:param downloader: 下载器
"""
# 获取下载器
server: Transmission = self.get_instance(downloader)
server = self.get_instance(downloader)
if not server:
return None
# 获取原标签
@@ -425,7 +454,7 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
server.set_torrent_tag(ids=hashs, tags=tags)
return None
def remove_torrents(self, hashs: Union[str, list], delete_file: Optional[bool] = True,
def remove_torrents(self, hashs: Union[str, list[str]], delete_file: Optional[bool] = True,
downloader: Optional[str] = None) -> Optional[bool]:
"""
删除下载器种子
@@ -435,12 +464,12 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
:return: bool
"""
# 获取下载器
server: Transmission = self.get_instance(downloader)
server = self.get_instance(downloader)
if not server:
return None
return server.delete_torrents(delete_file=delete_file, ids=hashs)
def set_torrents_tag(self, hashs: Union[str, list], tags: list,
def set_torrents_tag(self, hashs: Union[str, list[str]], tags: list[str],
downloader: Optional[str] = None) -> Optional[bool]:
"""
设置种子标签
@@ -450,7 +479,7 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
:return: bool
"""
# 获取下载器
server: Transmission = self.get_instance(downloader)
server = self.get_instance(downloader)
if not server:
return None
# 获取原标签,TR默认会覆盖,需追加
@@ -463,7 +492,7 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
downloader: Optional[str] = None,
download_limit: Optional[float] = None,
upload_limit: Optional[float] = None,
tracker_list: Optional[list] = None,
tracker_list: Optional[list[str]] = None,
save_path: Optional[str] = None,
category: Optional[str] = None,
ratio_limit: Optional[float] = None,
@@ -482,7 +511,7 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
:param seeding_time_limit: 做种时间限制,单位分钟
:return: 各项修改结果
"""
server: Transmission = self.get_instance(downloader)
server = self.get_instance(downloader)
if not server:
return None
results = {}
@@ -515,20 +544,21 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
self,
hash_string: str,
downloader: Optional[str] = None,
) -> Optional[Dict[str, List[str]]]:
) -> Optional[Dict[Optional[str], List[str]]]:
"""
查询下载任务Tracker列表。
:param hash_string: 种子Hash
:param downloader: 下载器
:return: 下载器名称到Tracker列表的映射
"""
servers: Dict[Optional[str], Transmission]
if downloader:
server: Transmission = self.get_instance(downloader)
server = self.get_instance(downloader)
if not server:
return None
servers = {downloader: server}
else:
servers: Dict[str, Transmission] = self.get_instances()
servers = self.get_instances()
ret_trackers = {}
for name, server in servers.items():
trackers = server.get_trackers(hash_string)
@@ -536,7 +566,7 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
ret_trackers[name] = trackers
return ret_trackers
def start_torrents(self, hashs: Union[list, str],
def start_torrents(self, hashs: Union[list[str], str],
downloader: Optional[str] = None) -> Optional[bool]:
"""
开始下载
@@ -545,12 +575,12 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
:return: bool
"""
# 获取下载器
server: Transmission = self.get_instance(downloader)
server = self.get_instance(downloader)
if not server:
return None
return server.start_torrents(ids=hashs)
def stop_torrents(self, hashs: Union[list, str],
def stop_torrents(self, hashs: Union[list[str], str],
downloader: Optional[str] = None) -> Optional[bool]:
"""
停止下载
@@ -559,7 +589,7 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
:return: bool
"""
# 获取下载器
server: Transmission = self.get_instance(downloader)
server = self.get_instance(downloader)
if not server:
return None
return server.stop_torrents(ids=hashs)
@@ -571,7 +601,7 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
获取种子文件列表,并在模块边界隔离 Transmission SDK 对象。
"""
# 获取下载器
server: Transmission = self.get_instance(downloader)
server = self.get_instance(downloader)
if not server:
return None
return self._normalize_torrent_files(
@@ -582,8 +612,9 @@ class TransmissionModule(_DownloaderModuleBase[Transmission]):
"""
下载器信息
"""
servers: Iterable[Transmission]
if downloader:
server: Transmission = self.get_instance(downloader)
server = self.get_instance(downloader)
if not server:
return None
servers = [server]