From 829d7944b06c1016f4b72dd4ad487feb6a8e4d8a Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 10 Jun 2026 07:07:33 +0800 Subject: [PATCH] fix: create temp directory for subtitle API downloads --- app/chain/download.py | 1 + app/modules/subtitle/__init__.py | 19 ++++-- tests/test_download_chain.py | 81 ++++++++++++++++++++++++- tests/test_subtitle_links.py | 101 ------------------------------- 4 files changed, 93 insertions(+), 109 deletions(-) diff --git a/app/chain/download.py b/app/chain/download.py index 9b897c43..f1e70939 100644 --- a/app/chain/download.py +++ b/app/chain/download.py @@ -148,6 +148,7 @@ class DownloadChain(ChainBase): return [] saved_files = [] + settings.TEMP_PATH.mkdir(parents=True, exist_ok=True) temp_file = settings.TEMP_PATH / file_name temp_extract_dir = temp_file.with_name(temp_file.stem) try: diff --git a/app/modules/subtitle/__init__.py b/app/modules/subtitle/__init__.py index 17564f1f..d305ccf3 100644 --- a/app/modules/subtitle/__init__.py +++ b/app/modules/subtitle/__init__.py @@ -202,18 +202,25 @@ class SubtitleModule(_ModuleBase): fileURI = FileURI.from_uri(download_dir.as_posix()) storage = fileURI.storage download_dir = Path(fileURI.path) - target_dir = download_dir / folder_name if folder_name else download_dir for _ in range(30): - found = storageChain.get_file_item(storage, target_dir) + found = storageChain.get_file_item(storage, download_dir / folder_name) if found: working_dir_item = found break time.sleep(1) - # 下载器可能还未创建保存目录,字幕保存前需要按完整目标路径补齐目录。 + # 目录仍然不存在,且有文件夹名,则创建目录 + if not working_dir_item and folder_name: + parent_dir_item = storageChain.get_folder(storage, download_dir) + if parent_dir_item: + working_dir_item = storageChain.create_folder( + parent_dir_item, + folder_name + ) + else: + logger.error(f"下载根目录不存在,无法创建字幕文件夹:{download_dir}") + return if not working_dir_item: - working_dir_item = storageChain.get_folder(storage, target_dir) - if not working_dir_item: - logger.error(f"下载目录不存在,无法保存字幕:{target_dir}") + logger.error(f"下载目录不存在,无法保存字幕:{download_dir / folder_name}") return # 读取网站代码 sublink_list = self._get_subtitle_links(torrent) diff --git a/tests/test_download_chain.py b/tests/test_download_chain.py index d492c898..07f64bdd 100644 --- a/tests/test_download_chain.py +++ b/tests/test_download_chain.py @@ -4,9 +4,10 @@ from unittest.mock import MagicMock import app.chain.download as download_module from app.chain.download import DownloadChain -from app.core.context import Context, MediaInfo, TorrentInfo +from app.core.config import settings +from app.core.context import Context, MediaInfo, SubtitleInfo, TorrentInfo from app.core.metainfo import MetaInfo -from app.schemas import NotExistMediaInfo +from app.schemas import FileItem, NotExistMediaInfo from app.schemas.types import MediaType @@ -42,6 +43,50 @@ class _FakeThreadHelper: self.submitted.append((func, args, kwargs)) +class _FakeSubtitleStorageChain: + """ + 模拟字幕 API 保存文件时使用的存储链。 + """ + + def __init__(self): + """ + 初始化上传记录。 + """ + self.uploaded_files = [] + + def get_folder(self, storage, path): + """ + 模拟目标目录存在或已创建。 + """ + return FileItem(storage=storage, type="dir", path=path.as_posix(), name=path.name) + + def get_file_item(self, _storage, _path): + """ + 模拟目标字幕文件不存在。 + """ + return None + + def upload_file(self, fileitem, path): + """ + 记录上传的临时字幕文件。 + """ + self.uploaded_files.append(path) + return FileItem( + storage=fileitem.storage, + type="file", + path=(Path(fileitem.path) / path.name).as_posix(), + name=path.name, + ) + + +class _FakeSubtitleResponse: + """ + 模拟字幕 API 下载响应。 + """ + + content = b"subtitle-content" + + def test_download_single_submits_download_added_to_background(monkeypatch): """ 添加下载成功后,站点字幕等后处理应提交到后台,不能阻塞下载接口返回。 @@ -99,6 +144,38 @@ def test_download_single_submits_download_added_to_background(monkeypatch): ) +def test_save_subtitle_response_creates_missing_temp_directory(monkeypatch, tmp_path): + """ + 下载字幕 API 保存响应前应自动创建缺失的临时目录。 + """ + storage_chain = _FakeSubtitleStorageChain() + temp_path = tmp_path / "missing-temp" + assert not temp_path.exists() + + monkeypatch.setattr( + download_module, + "settings", + SimpleNamespace(TEMP_PATH=temp_path, RMT_SUBEXT=settings.RMT_SUBEXT), + ) + monkeypatch.setattr(download_module, "StorageChain", lambda: storage_chain) + chain = DownloadChain.__new__(DownloadChain) + subtitle = SubtitleInfo( + title="Demo Movie", + enclosure="https://example.test/subtitle.srt", + file_name="Demo.Movie.zh-cn.srt", + ) + + saved_files = chain._save_subtitle_response( + subtitle=subtitle, + response=_FakeSubtitleResponse(), + target_dir=Path("/downloads"), + ) + + assert temp_path.exists() + assert saved_files == ["/downloads/Demo.Movie.zh-cn.srt"] + assert storage_chain.uploaded_files + + class _FakeBatchTorrentHelper: """ 为批量下载测试提供稳定排序和种子文件集数解析。 diff --git a/tests/test_subtitle_links.py b/tests/test_subtitle_links.py index 02f0c879..745c3eac 100644 --- a/tests/test_subtitle_links.py +++ b/tests/test_subtitle_links.py @@ -1,66 +1,9 @@ -from pathlib import Path - from lxml import etree -from app.core.context import Context, TorrentInfo from app.modules.subtitle import SubtitleModule -from app.schemas.file import FileItem - - -class _FakeStorageChain: - """ - 模拟字幕下载使用的存储链,记录目录查询与创建行为。 - """ - - def __init__(self): - """ - 初始化调用记录。 - """ - self.file_item_paths = [] - self.created_paths = [] - self.uploaded_files = [] - - def get_file_item(self, storage, path): - """ - 记录文件项查询,模拟目标目录和字幕文件都不存在。 - """ - self.file_item_paths.append((storage, path)) - return None - - def get_folder(self, storage, path): - """ - 记录递归创建目录请求,并返回创建后的目录项。 - """ - self.created_paths.append((storage, path)) - return FileItem(storage=storage, type="dir", path=path.as_posix(), name=path.name) - - def upload_file(self, fileitem, path): - """ - 记录上传请求,并返回上传后的文件项。 - """ - self.uploaded_files.append((fileitem, path)) - return FileItem( - storage=fileitem.storage, - type="file", - path=(path.parent / path.name).as_posix(), - name=path.name, - ) - - -class _FakeResponse: - """ - 构造字幕下载响应对象。 - """ - - status_code = 200 - content = b"subtitle" - headers = {"Content-Disposition": 'attachment; filename="example.srt"'} def test_parse_subtitle_links_filters_detail_page_action_links(): - """ - 详情页解析字幕链接时应过滤上传和外部搜索动作链接。 - """ html_text = """ 字幕 @@ -100,47 +43,3 @@ def test_parse_subtitle_links_filters_detail_page_action_links(): assert links == [ "https://audiences.me/downloadsubs.php?torrentid=621182&subid=2148" ] - - -def test_download_added_creates_missing_target_directory(monkeypatch): - """ - 下载字幕时目标目录不存在,应按完整下载目录自动创建后再保存字幕。 - """ - storage_chain = _FakeStorageChain() - module = SubtitleModule() - context = Context( - torrent_info=TorrentInfo( - page_url="https://example.test/details.php?id=1", - site_cookie="cookie", - site_ua="ua", - ) - ) - - monkeypatch.setattr("app.modules.subtitle.StorageChain", lambda: storage_chain) - monkeypatch.setattr( - "app.modules.subtitle.TorrentHelper.get_fileinfo_from_torrent_content", - lambda self, content: ("Example Folder", []), - ) - monkeypatch.setattr( - "app.modules.subtitle.TorrentHelper.get_url_filename", - lambda response, url: "example.srt", - ) - monkeypatch.setattr( - "app.modules.subtitle.RequestUtils", - lambda **kwargs: type( - "FakeRequest", - (), - {"get_res": lambda self, url: _FakeResponse()}, - )(), - ) - monkeypatch.setattr("app.modules.subtitle.time.sleep", lambda seconds: None) - monkeypatch.setattr(module, "_get_subtitle_links", lambda torrent: ["https://example.test/subtitle.srt"]) - - module.download_added( - context=context, - download_dir=Path("/downloads"), - torrent_content=b"torrent", - ) - - assert storage_chain.created_paths == [("local", Path("/downloads/Example Folder"))] - assert storage_chain.uploaded_files