From e51b67938078215e9724f5601642f953a968ba2d Mon Sep 17 00:00:00 2001 From: stkevintan Date: Fri, 28 Nov 2025 14:55:01 +0800 Subject: [PATCH 1/3] fix: support non-local filesystem operations for default dir and subtitles --- app/chain/__init__.py | 6 ++++-- app/chain/download.py | 9 ++++++-- app/helper/directory.py | 29 ++++++++++++++++++------- app/modules/subtitle/__init__.py | 37 ++++++++++++++++++++++---------- 4 files changed, 58 insertions(+), 23 deletions(-) diff --git a/app/chain/__init__.py b/app/chain/__init__.py index 2ee4f0e5..c0dbe2a9 100644 --- a/app/chain/__init__.py +++ b/app/chain/__init__.py @@ -707,17 +707,19 @@ class ChainBase(metaclass=ABCMeta): cookie=cookie, episodes=episodes, category=category, label=label, downloader=downloader) - def download_added(self, context: Context, download_dir: Path, torrent_content: Union[str, bytes] = None) -> None: + def download_added(self, context: Context, download_dir: Path, storage: str, torrent_content: Union[str, bytes] = None) -> None: """ 添加下载任务成功后,从站点下载字幕,保存到下载目录 :param context: 上下文,包括识别信息、媒体信息、种子信息 :param download_dir: 下载目录 + :param storage: 存储类型 :param torrent_content: 种子内容,如果有则直接使用该内容,否则从context中获取种子文件路径 :return: None,该方法可被多个模块同时处理 """ return self.run_module("download_added", context=context, torrent_content=torrent_content, - download_dir=download_dir) + download_dir=download_dir, + storage=storage) def list_torrents(self, status: TorrentStatus = None, hashs: Union[list, str] = None, diff --git a/app/chain/download.py b/app/chain/download.py index db2c4f25..0b012d40 100644 --- a/app/chain/download.py +++ b/app/chain/download.py @@ -232,13 +232,18 @@ class DownloadChain(ChainBase): # 获取种子文件的文件夹名和文件清单 _folder_name, _file_list = TorrentHelper().get_fileinfo_from_torrent_content(torrent_content) + storage = 'local' # 下载目录 if save_path: # 下载目录使用自定义的 download_dir = Path(save_path) + # Check if the download_dir matches any configured dirs + dir_info = DirectoryHelper().get_dir(dest_path=download_dir) + storage = dir_info.library_storage if dir_info else storage else: # 根据媒体信息查询下载目录配置 - dir_info = DirectoryHelper().get_dir(_media, storage="local", include_unsorted=True) + dir_info = DirectoryHelper().get_dir(_media, include_unsorted=True) + storage = dir_info.library_storage if dir_info else storage # 拼装子目录 if dir_info: # 一级目录 @@ -358,7 +363,7 @@ class DownloadChain(ChainBase): username=username, ) # 下载成功后处理 - self.download_added(context=context, download_dir=download_dir, torrent_content=torrent_content) + self.download_added(context=context, download_dir=download_dir, storage=storage, torrent_content=torrent_content) # 广播事件 self.eventmanager.send_event(EventType.DownloadAdded, { "hash": _hash, diff --git a/app/helper/directory.py b/app/helper/directory.py index 5af67197..370f77c4 100644 --- a/app/helper/directory.py +++ b/app/helper/directory.py @@ -1,6 +1,6 @@ import re from pathlib import Path -from typing import List, Optional +from typing import List, Optional, Tuple from app import schemas from app.core.context import MediaInfo @@ -51,7 +51,7 @@ class DirectoryHelper: """ return [d for d in self.get_library_dirs() if d.library_storage == "local"] - def get_dir(self, media: MediaInfo, include_unsorted: Optional[bool] = False, + def get_dir(self, media: Optional[MediaInfo], include_unsorted: Optional[bool] = False, storage: Optional[str] = None, src_path: Path = None, target_storage: Optional[str] = None, dest_path: Path = None ) -> Optional[schemas.TransferDirectoryConf]: @@ -64,11 +64,8 @@ class DirectoryHelper: :param src_path: 源目录,有值时直接匹配 :param dest_path: 目标目录,有值时直接匹配 """ - # 处理类型 - if not media: - return None # 电影/电视剧 - media_type = media.type.value + media_type = media.type.value if media else None dirs = self.get_dirs() # 如果存在源目录,并源目录为任一下载目录的子目录时,则进行源目录匹配,否则,允许源目录按同盘优先的逻辑匹配 @@ -93,7 +90,7 @@ class DirectoryHelper: if dest_path and dest_path != Path(d.library_path): continue # 目录类型为全部的,符合条件 - if not d.media_type: + if not media_type or not d.media_type: matched_dirs.append(d) continue # 目录类型相等,目录类别为全部,符合条件 @@ -109,11 +106,27 @@ class DirectoryHelper: # 优先源目录同盘 for matched_dir in matched_dirs: matched_path = Path(matched_dir.download_path) - if SystemUtils.is_same_disk(matched_path, src_path): + if self._is_same_source((src_path, storage or "local"), (matched_path, matched_dir.library_storage)): return matched_dir return matched_dirs[0] return None + @staticmethod + def _is_same_source(src: Tuple[Path, str], tar: Tuple[Path, str]) -> bool: + """ + 判断源目录和目标目录是否在同一存储盘 + + :param src: 源目录路径和存储类型 + :param tar: 目标目录路径和存储类型 + :return: 是否在同一存储盘 + """ + src_path, src_storage = src + tar_path, tar_storage = tar + if "local" == tar_storage == src_storage: + return SystemUtils.is_same_disk(src_path, tar_path) + # 网络存储,直接比较类型 + return src_storage == tar_storage + @staticmethod def get_media_root_path(rename_format: str, rename_path: Path) -> Optional[Path]: """ diff --git a/app/modules/subtitle/__init__.py b/app/modules/subtitle/__init__.py index 83b2f66e..211c0bb1 100644 --- a/app/modules/subtitle/__init__.py +++ b/app/modules/subtitle/__init__.py @@ -5,11 +5,13 @@ from typing import Tuple, Union from lxml import etree +from app.chain.storage import StorageChain from app.core.config import settings from app.core.context import Context from app.helper.torrent import TorrentHelper from app.log import logger from app.modules import _ModuleBase +from app.schemas.file import FileItem from app.schemas.types import ModuleType, OtherModulesType from app.utils.http import RequestUtils from app.utils.string import StringUtils @@ -63,11 +65,12 @@ class SubtitleModule(_ModuleBase): def test(self): pass - def download_added(self, context: Context, download_dir: Path, torrent_content: Union[str, bytes] = None): + def download_added(self, context: Context, download_dir: Path, storage: str, torrent_content: Union[str, bytes] = None): """ 添加下载任务成功后,从站点下载字幕,保存到下载目录 :param context: 上下文,包括识别信息、媒体信息、种子信息 :param download_dir: 下载目录 + :param storage: 存储类型 :param torrent_content: 种子内容,如果是种子文件,则为文件内容,否则为种子字符串 :return: None,该方法可被多个模块同时处理 """ @@ -87,15 +90,24 @@ class SubtitleModule(_ModuleBase): # 获取种子信息 folder_name, _ = TorrentHelper().get_fileinfo_from_torrent_content(torrent_content) # 文件保存目录,如果是单文件种子,则folder_name是空,此时文件保存目录就是下载目录 - download_dir = download_dir / folder_name + storageChain = StorageChain() # 等待目录存在 + working_dir_item = None for _ in range(30): - if download_dir.exists(): + found = storageChain.get_file_item(storage, download_dir / folder_name) + if found: + working_dir_item = found break time.sleep(1) # 目录仍然不存在,且有文件夹名,则创建目录 - if not download_dir.exists() and folder_name: - download_dir.mkdir(parents=True, exist_ok=True) + if not working_dir_item and folder_name: + working_dir_item = storageChain.create_folder( + storageChain.get_file_item(storage, download_dir), + folder_name + ) + if not working_dir_item: + logger.error(f"下载目录不存在,无法保存字幕:{download_dir / folder_name}") + return # 读取网站代码 request = RequestUtils(cookies=torrent.site_cookie, ua=torrent.site_ua) res = request.get_res(torrent.page_url) @@ -144,12 +156,12 @@ class SubtitleModule(_ModuleBase): shutil.unpack_archive(zip_file, zip_path, format='zip') # 遍历转移文件 for sub_file in SystemUtils.list_files(zip_path, settings.RMT_SUBEXT): - target_sub_file = download_dir / sub_file.name - if target_sub_file.exists(): + target_sub_file = Path(working_dir_item.path) / Path(sub_file.name) + if storageChain.get_file_item(storage, target_sub_file): logger.info(f"字幕文件已存在:{target_sub_file}") continue logger.info(f"转移字幕 {sub_file} 到 {target_sub_file} ...") - SystemUtils.copy(sub_file, target_sub_file) + storageChain.upload_file(working_dir_item, sub_file) # 删除临时文件 try: shutil.rmtree(zip_path) @@ -160,9 +172,12 @@ class SubtitleModule(_ModuleBase): sub_file = settings.TEMP_PATH / file_name # 保存 sub_file.write_bytes(ret.content) - target_sub_file = download_dir / sub_file.name - logger.info(f"转移字幕 {sub_file} 到 {target_sub_file}") - SystemUtils.copy(sub_file, target_sub_file) + target_sub_file = Path(working_dir_item.path) / Path(sub_file.name) + if storageChain.get_file_item(storage, target_sub_file): + logger.info(f"字幕文件已存在:{target_sub_file}") + continue + logger.info(f"转移字幕 {sub_file} 到 {target_sub_file} ...") + storageChain.upload_file(working_dir_item, sub_file) else: logger.error(f"下载字幕文件失败:{sublink}") continue From 18096909155c30b806bbd2afd549d7d1c6d04765 Mon Sep 17 00:00:00 2001 From: Kevin Tan Date: Fri, 28 Nov 2025 17:21:17 +0800 Subject: [PATCH 2/3] Update app/modules/subtitle/__init__.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- app/modules/subtitle/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/modules/subtitle/__init__.py b/app/modules/subtitle/__init__.py index 211c0bb1..8108d240 100644 --- a/app/modules/subtitle/__init__.py +++ b/app/modules/subtitle/__init__.py @@ -101,10 +101,15 @@ class SubtitleModule(_ModuleBase): time.sleep(1) # 目录仍然不存在,且有文件夹名,则创建目录 if not working_dir_item and folder_name: - working_dir_item = storageChain.create_folder( - storageChain.get_file_item(storage, download_dir), - folder_name - ) + parent_dir_item = storageChain.get_file_item(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: logger.error(f"下载目录不存在,无法保存字幕:{download_dir / folder_name}") return From 36bdb831bec4497796b94a123a0156219f52a04b Mon Sep 17 00:00:00 2001 From: stkevintan Date: Fri, 28 Nov 2025 19:30:17 +0800 Subject: [PATCH 3/3] use download storage instead of library storage --- app/chain/download.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/chain/download.py b/app/chain/download.py index 0b012d40..8d2e370d 100644 --- a/app/chain/download.py +++ b/app/chain/download.py @@ -239,11 +239,11 @@ class DownloadChain(ChainBase): download_dir = Path(save_path) # Check if the download_dir matches any configured dirs dir_info = DirectoryHelper().get_dir(dest_path=download_dir) - storage = dir_info.library_storage if dir_info else storage + storage = dir_info.storage if dir_info else storage else: # 根据媒体信息查询下载目录配置 dir_info = DirectoryHelper().get_dir(_media, include_unsorted=True) - storage = dir_info.library_storage if dir_info else storage + storage = dir_info.storage if dir_info else storage # 拼装子目录 if dir_info: # 一级目录