Files
MoviePilot/app/startup/initializers/network.py

228 lines
7.1 KiB
Python

"""装配 Chain 使用的同步网络与相关系统窄端口。"""
from collections.abc import Mapping
from pathlib import Path
from typing import Any, Optional, Union, cast
from app.adapters.network.http import RequestUtils
from app.adapters.system.host import SystemUtils
from app.chain.download.ports import (
DownloadArchivePort,
DownloadHttpPort,
DownloadResponsePort,
configure_download_ports,
reset_download_ports,
)
from app.chain.message import (
MessageHttpPort,
MessageResponsePort,
configure_message_http_port,
reset_message_http_port,
)
from app.chain.scraping import (
ScrapingHttpPort,
ScrapingResponsePort,
ScrapingStreamResponsePort,
configure_scraping_http_port,
reset_scraping_http_port,
)
from app.chain.system import (
SystemEnvironmentPort,
SystemHttpPort,
SystemResponsePort,
configure_system_ports,
reset_system_ports,
)
class _DownloadHttpAdapter:
"""把 RequestUtils 收窄为下载链同步 HTTP 端口。"""
@staticmethod
def _request(
*,
cookies: Optional[Union[str, dict[str, str]]],
ua: Optional[str],
headers: Optional[dict[str, str]],
proxies: Optional[dict[str, str]],
timeout: Optional[int],
) -> RequestUtils:
"""按下载链传入的代理、认证与超时参数构造一次请求。"""
options: dict[str, Any] = {
"cookies": cookies,
"ua": ua,
"headers": headers,
"proxies": proxies,
"timeout": timeout,
}
return RequestUtils(**options)
def get(
self,
url: str,
*,
cookies: Optional[Union[str, dict[str, str]]] = None,
ua: Optional[str] = None,
headers: Optional[dict[str, str]] = None,
proxies: Optional[dict[str, str]] = None,
timeout: Optional[int] = None,
params: Optional[dict[str, Any]] = None,
raise_exception: bool = False,
) -> Optional[DownloadResponsePort]:
"""发送下载链 GET 请求并原样保留响应三态。"""
request = self._request(
cookies=cookies,
ua=ua,
headers=headers,
proxies=proxies,
timeout=timeout,
)
kwargs: dict[str, Any] = {"raise_exception": raise_exception}
if params is not None:
kwargs["params"] = params
response = request.get_res(url, **kwargs)
return cast(Optional[DownloadResponsePort], response)
def post(
self,
url: str,
*,
cookies: Optional[Union[str, dict[str, str]]] = None,
ua: Optional[str] = None,
headers: Optional[dict[str, str]] = None,
proxies: Optional[dict[str, str]] = None,
timeout: Optional[int] = None,
params: Optional[dict[str, Any]] = None,
) -> Optional[DownloadResponsePort]:
"""发送下载链 POST 请求并原样保留响应三态。"""
request = self._request(
cookies=cookies,
ua=ua,
headers=headers,
proxies=proxies,
timeout=timeout,
)
kwargs: dict[str, Any] = {}
if params is not None:
kwargs["params"] = params
response = request.post_res(url, **kwargs)
return cast(Optional[DownloadResponsePort], response)
class _DownloadArchiveAdapter:
"""把 SystemUtils 收窄为下载链字幕归档端口。"""
def unpack(
self,
archive_file: Path,
extract_dir: Path,
*,
archive_format: Optional[str],
) -> None:
"""使用宿主统一归档实现解压字幕文件。"""
SystemUtils.unpack_archive(
archive_file,
extract_dir,
archive_format=archive_format,
)
def list_files(self, directory: Path, extensions: tuple[str, ...]) -> list[Path]:
"""使用宿主统一文件扫描实现列出字幕文件。"""
return SystemUtils.list_files(directory, list(extensions))
class _MessageHttpAdapter:
"""把 RequestUtils 收窄为消息附件同步 GET 端口。"""
def get(self, url: str, *, timeout: int) -> Optional[MessageResponsePort]:
"""按消息链固定超时读取附件响应。"""
response = RequestUtils(timeout=timeout).get_res(url)
return cast(Optional[MessageResponsePort], response)
class _ScrapingHttpAdapter:
"""把 RequestUtils 收窄为刮削链普通与流式 GET 端口。"""
def get(
self,
url: str,
*,
proxies: Optional[dict[str, str]],
ua: str,
timeout: int,
) -> Optional[ScrapingResponsePort]:
"""读取需要完整载荷的音乐封面响应。"""
options: dict[str, Any] = {
"proxies": proxies,
"ua": ua,
"timeout": timeout,
}
response = RequestUtils(**options).get_res(url)
return cast(Optional[ScrapingResponsePort], response)
def stream(
self,
url: str,
*,
proxies: Optional[dict[str, str]],
ua: str,
) -> ScrapingStreamResponsePort:
"""打开由刮削链上下文关闭的流式图片响应。"""
options: dict[str, Any] = {"proxies": proxies, "ua": ua}
response = RequestUtils(**options).get_stream(url=url)
return cast(ScrapingStreamResponsePort, response)
class _SystemHttpAdapter:
"""把 RequestUtils 收窄为系统链发布版本 GET 端口。"""
def get(
self,
url: str,
*,
proxies: Optional[dict[str, str]],
headers: Mapping[str, str],
) -> Optional[SystemResponsePort]:
"""按系统配置的代理与 GitHub 请求头读取发布列表。"""
options: dict[str, Any] = {
"proxies": proxies,
"headers": dict(headers),
}
response = RequestUtils(**options).get_res(url)
return cast(Optional[SystemResponsePort], response)
class _SystemEnvironmentAdapter:
"""把 SystemUtils 收窄为系统链容器环境判断端口。"""
def is_docker(self) -> bool:
"""返回宿主统一环境探针的 Docker 判断。"""
return bool(SystemUtils.is_docker())
def init_chain_network_ports() -> None:
"""原子式装配四个 Chain 的六条 Adapter 静态依赖边。"""
reset_chain_network_ports()
try:
configure_download_ports(
http=cast(DownloadHttpPort, _DownloadHttpAdapter()),
archive=cast(DownloadArchivePort, _DownloadArchiveAdapter()),
)
configure_message_http_port(cast(MessageHttpPort, _MessageHttpAdapter()))
configure_scraping_http_port(cast(ScrapingHttpPort, _ScrapingHttpAdapter()))
configure_system_ports(
http=cast(SystemHttpPort, _SystemHttpAdapter()),
environment=cast(SystemEnvironmentPort, _SystemEnvironmentAdapter()),
)
except Exception:
reset_chain_network_ports()
raise
def reset_chain_network_ports() -> None:
"""清除四个 Chain 的技术端口,支持重复 lifespan 与失败回滚。"""
reset_system_ports()
reset_scraping_http_port()
reset_message_http_port()
reset_download_ports()