mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 04:47:03 +08:00
fix(download): validate torrent bytes before html fallback
This commit is contained in:
@@ -2,7 +2,7 @@ import datetime
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Protocol, Tuple, Union
|
||||
from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple, Union
|
||||
from urllib.parse import unquote
|
||||
|
||||
from torrentool.api import Torrent
|
||||
@@ -119,6 +119,19 @@ def _parse_filter_size_range(size_range: str) -> Tuple[str, float, Optional[floa
|
||||
return "unknown", 0, None
|
||||
|
||||
|
||||
def _cache_valid_torrent_content(
|
||||
parser: Callable[[Union[str, bytes]], Tuple[str, List[str]]],
|
||||
cache_backend: Any,
|
||||
cache_path: Path,
|
||||
content: bytes,
|
||||
) -> Tuple[str, List[str]]:
|
||||
"""解析合法种子并缓存原始二进制内容。"""
|
||||
folder_name, file_list = parser(content)
|
||||
if file_list:
|
||||
cache_backend.set(cache_path.as_posix(), content, region="torrents")
|
||||
return folder_name, file_list
|
||||
|
||||
|
||||
class TorrentHelper:
|
||||
"""
|
||||
种子帮助类
|
||||
@@ -187,6 +200,13 @@ class TorrentHelper:
|
||||
if req.content.startswith(b"magnet:"):
|
||||
# 磁力链接
|
||||
return cache_path, req.text, "", [], "获取到磁力链接"
|
||||
# 优先按原始二进制解析,避免合法 torrent 被误判为首次下载 HTML。
|
||||
folder_name, file_list = _cache_valid_torrent_content(
|
||||
self.get_fileinfo_from_torrent_content, cache_backend, cache_path, req.content
|
||||
)
|
||||
if file_list:
|
||||
# 成功拿到种子数据
|
||||
return cache_path, req.content, folder_name, file_list, ""
|
||||
if "下载种子文件".encode("utf-8") in req.content:
|
||||
# 首次下载提示页面
|
||||
skip_flag = False
|
||||
@@ -224,24 +244,17 @@ class TorrentHelper:
|
||||
logger.warn(f"触发了站点首次种子下载,尝试自动跳过时出现错误:{str(err)}")
|
||||
if not skip_flag:
|
||||
return cache_path, None, "", [], "种子数据有误,请确认链接是否正确,如为PT站点则需手工在站点下载一次种子"
|
||||
# 首次下载提示页面完成后,重新按原始二进制解析 POST 响应。
|
||||
assert req is not None
|
||||
# 种子内容
|
||||
if req.content:
|
||||
# 检查是不是种子文件,如果不是仍然抛出异常
|
||||
try:
|
||||
# 获取种子目录和文件清单
|
||||
folder_name, file_list = self.get_fileinfo_from_torrent_content(req.content)
|
||||
if file_list:
|
||||
# 保存到缓存
|
||||
cache_backend.set(cache_path.as_posix(), req.content, region="torrents")
|
||||
folder_name, file_list = _cache_valid_torrent_content(
|
||||
self.get_fileinfo_from_torrent_content, cache_backend, cache_path, req.content
|
||||
)
|
||||
if file_list:
|
||||
# 成功拿到种子数据
|
||||
return cache_path, req.content, folder_name, file_list, ""
|
||||
except Exception as err:
|
||||
logger.error(f"种子文件解析失败:{str(err)}")
|
||||
# 种子数据仍然错误
|
||||
return cache_path, None, "", [], "种子数据有误,请确认链接是否正确"
|
||||
# 返回失败
|
||||
return cache_path, None, "", [], ""
|
||||
# 解析失败时终止,避免把 HTML 继续下沉到下载器或缓存。
|
||||
return cache_path, None, "", [], "种子数据有误,请确认链接是否正确"
|
||||
elif req is None:
|
||||
return cache_path, None, "", [], "无法打开链接"
|
||||
elif req.status_code == 429:
|
||||
|
||||
@@ -125,6 +125,79 @@ def test_torrent_fake_port_preserves_rate_limit_message(monkeypatch) -> None:
|
||||
assert result[1:] == (None, "", [], "触发站点流控,请稍后重试")
|
||||
|
||||
|
||||
def test_torrent_binary_is_parsed_before_first_download_html_detection(monkeypatch) -> None:
|
||||
"""合法二进制种子包含首次下载提示文本时仍应按 torrent 解析。"""
|
||||
marker = "下载种子文件".encode("utf-8")
|
||||
torrent_content = (
|
||||
b"d4:infod7:comment"
|
||||
+ str(len(marker)).encode("ascii")
|
||||
+ b":"
|
||||
+ marker
|
||||
+ b"4:name7:foo.mkv6:lengthi1eee"
|
||||
)
|
||||
cached: list[bytes] = []
|
||||
|
||||
class Cache:
|
||||
"""记录合法种子缓存写入,不接触真实缓存后端。"""
|
||||
|
||||
@staticmethod
|
||||
def get(*_args, **_kwargs):
|
||||
"""固定返回缓存未命中。"""
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def set(_key, value, **_kwargs):
|
||||
"""记录原始二进制种子内容。"""
|
||||
cached.append(value)
|
||||
|
||||
class HttpPort:
|
||||
"""返回包含提示文本的合法二进制种子响应。"""
|
||||
|
||||
@staticmethod
|
||||
def request(**_kwargs):
|
||||
"""返回固定 torrent 响应。"""
|
||||
return SimpleNamespace(
|
||||
status_code=200,
|
||||
headers={"Content-Type": "application/x-bittorrent"},
|
||||
content=torrent_content,
|
||||
text=torrent_content.decode("utf-8"),
|
||||
reason="",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(torrent_module, "FileCache", Cache)
|
||||
configure_torrent_port(HttpPort())
|
||||
|
||||
result = TorrentHelper().download_torrent("https://example.test/download/1")
|
||||
|
||||
assert result[1:] == (torrent_content, "", ["foo.mkv"], "")
|
||||
assert cached == [torrent_content]
|
||||
|
||||
|
||||
def test_torrent_invalid_http_200_body_is_rejected_before_downloader(monkeypatch) -> None:
|
||||
"""HTTP 200 返回非种子内容时应在下载层终止并避免缓存。"""
|
||||
|
||||
class HttpPort:
|
||||
"""返回登录页面的种子 HTTP 假端口。"""
|
||||
|
||||
@staticmethod
|
||||
def request(**_kwargs):
|
||||
"""返回固定 HTML 响应。"""
|
||||
return SimpleNamespace(
|
||||
status_code=200,
|
||||
headers={"Content-Type": "text/html"},
|
||||
content=b"<html>login</html>",
|
||||
text="<html>login</html>",
|
||||
reason="",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(torrent_module, "FileCache", _EmptyCache)
|
||||
configure_torrent_port(HttpPort())
|
||||
|
||||
result = TorrentHelper().download_torrent("https://example.test/download/2")
|
||||
|
||||
assert result[1:] == (None, "", [], "种子数据有误,请确认链接是否正确")
|
||||
|
||||
|
||||
def test_cookie_invalid_parameters_keep_legacy_result_without_ports() -> None:
|
||||
"""参数错误属于 Application 语义,不应依赖浏览器端口已装配。"""
|
||||
assert CookieHelper().get_site_cookie_ua("", "", "") == (
|
||||
|
||||
Reference in New Issue
Block a user