From 378ba51f4de6d24e77249e6e9efebcff30848043 Mon Sep 17 00:00:00 2001 From: stkevintan Date: Mon, 8 Dec 2025 14:00:39 +0800 Subject: [PATCH 1/4] support path_mapping for downloader --- app/chain/__init__.py | 6 ++---- app/chain/download.py | 11 +++++------ app/modules/__init__.py | 25 +++++++++++++++++++++++++ app/modules/qbittorrent/__init__.py | 2 +- app/modules/subtitle/__init__.py | 9 ++++++--- app/modules/transmission/__init__.py | 2 +- app/schemas/system.py | 2 ++ 7 files changed, 42 insertions(+), 15 deletions(-) diff --git a/app/chain/__init__.py b/app/chain/__init__.py index c0dbe2a9..2ee4f0e5 100644 --- a/app/chain/__init__.py +++ b/app/chain/__init__.py @@ -707,19 +707,17 @@ class ChainBase(metaclass=ABCMeta): cookie=cookie, episodes=episodes, category=category, label=label, downloader=downloader) - def download_added(self, context: Context, download_dir: Path, storage: str, torrent_content: Union[str, bytes] = None) -> None: + def download_added(self, context: Context, download_dir: Path, 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, - storage=storage) + download_dir=download_dir) 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 f687228f..18672fb0 100644 --- a/app/chain/download.py +++ b/app/chain/download.py @@ -19,7 +19,7 @@ from app.db.mediaserver_oper import MediaServerOper from app.helper.directory import DirectoryHelper from app.helper.torrent import TorrentHelper from app.log import logger -from app.schemas import ExistMediaInfo, NotExistMediaInfo, DownloadingTorrent, Notification, ResourceSelectionEventData, \ +from app.schemas import ExistMediaInfo, FileURI, NotExistMediaInfo, DownloadingTorrent, Notification, ResourceSelectionEventData, \ ResourceDownloadEventData from app.schemas.types import MediaType, TorrentStatus, EventType, MessageChannel, NotificationType, ContentType, \ ChainEventType @@ -235,10 +235,7 @@ class DownloadChain(ChainBase): storage = 'local' # 下载目录 if save_path: - uri = schemas.FileURI.from_uri(save_path) - # 下载目录使用自定义的 - download_dir = Path(uri.path) - storage = uri.storage + download_dir = Path(save_path) else: # 根据媒体信息查询下载目录配置 dir_info = DirectoryHelper().get_dir(_media, include_unsorted=True) @@ -263,6 +260,8 @@ class DownloadChain(ChainBase): self.messagehelper.put(f"{_media.type.value} {_media.title_year} 未找到下载目录!", title="下载失败", role="system") return None + fileURI = FileURI(storage=storage, path=download_dir.as_posix()) + download_dir = Path(fileURI.uri) # 添加下载 result: Optional[tuple] = self.download(content=torrent_content, @@ -362,7 +361,7 @@ class DownloadChain(ChainBase): username=username, ) # 下载成功后处理 - self.download_added(context=context, download_dir=download_dir, storage=storage, torrent_content=torrent_content) + self.download_added(context=context, download_dir=download_dir, torrent_content=torrent_content) # 广播事件 self.eventmanager.send_event(EventType.DownloadAdded, { "hash": _hash, diff --git a/app/modules/__init__.py b/app/modules/__init__.py index 89fe2add..2fef7dc9 100644 --- a/app/modules/__init__.py +++ b/app/modules/__init__.py @@ -1,5 +1,6 @@ from abc import abstractmethod, ABCMeta from typing import Generic, Tuple, Union, TypeVar, Type, Dict, Optional, Callable +from pathlib import Path from app.helper.service import ServiceConfigHelper from app.schemas import Notification, NotificationConf, MediaServerConf, DownloaderConf @@ -290,6 +291,30 @@ class _DownloaderBase(ServiceBase[TService, DownloaderConf]): 重置默认配置名称 """ self._default_config_name = None + + def normalize_path(self, path: Path, downloader: Optional[str]) -> str: + """ + 根据下载器配置和路径映射,规范化下载路径 + + :param path: 原始路径 + :param conf: 下载器配置 + :return: 规范化后的路径 + """ + dir = path.as_posix() + conf = self.get_config(downloader) + if conf and conf.path_mapping: + for (src, dst) in conf.path_mapping: + src = Path(src.strip()).as_posix() + dst = Path(dst.strip()).as_posix() + if dir.startswith(src): + dir = dir.replace(src, dst, 1) + break + # 处理存储协议前缀 + for s in StorageSchema: + prefix = f"{s.value}:" + if dir.startswith(prefix): + return dir[len(prefix):] + return dir class _MediaServerBase(ServiceBase[TService, MediaServerConf]): diff --git a/app/modules/qbittorrent/__init__.py b/app/modules/qbittorrent/__init__.py index 4ee36440..ddb1fdeb 100644 --- a/app/modules/qbittorrent/__init__.py +++ b/app/modules/qbittorrent/__init__.py @@ -150,7 +150,7 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]): # 添加任务 state = server.add_torrent( content=content, - download_dir=download_dir.as_posix(), + download_dir=self.normalize_path(download_dir, downloader), is_paused=is_paused, tag=tags, cookie=cookie, diff --git a/app/modules/subtitle/__init__.py b/app/modules/subtitle/__init__.py index 8108d240..5b437aeb 100644 --- a/app/modules/subtitle/__init__.py +++ b/app/modules/subtitle/__init__.py @@ -11,7 +11,7 @@ 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.file import FileURI from app.schemas.types import ModuleType, OtherModulesType from app.utils.http import RequestUtils from app.utils.string import StringUtils @@ -65,12 +65,11 @@ class SubtitleModule(_ModuleBase): def test(self): pass - def download_added(self, context: Context, download_dir: Path, storage: str, torrent_content: Union[str, bytes] = None): + def download_added(self, context: Context, download_dir: Path, torrent_content: Union[str, bytes] = None): """ 添加下载任务成功后,从站点下载字幕,保存到下载目录 :param context: 上下文,包括识别信息、媒体信息、种子信息 :param download_dir: 下载目录 - :param storage: 存储类型 :param torrent_content: 种子内容,如果是种子文件,则为文件内容,否则为种子字符串 :return: None,该方法可被多个模块同时处理 """ @@ -93,6 +92,10 @@ class SubtitleModule(_ModuleBase): storageChain = StorageChain() # 等待目录存在 working_dir_item = None + # split download_dir into storage and path + fileURI = FileURI.from_uri(download_dir.as_posix()) + storage = fileURI.storage + download_dir = Path(fileURI.path) for _ in range(30): found = storageChain.get_file_item(storage, download_dir / folder_name) if found: diff --git a/app/modules/transmission/__init__.py b/app/modules/transmission/__init__.py index def26677..8b2da1a9 100644 --- a/app/modules/transmission/__init__.py +++ b/app/modules/transmission/__init__.py @@ -151,7 +151,7 @@ class TransmissionModule(_ModuleBase, _DownloaderBase[Transmission]): # 添加任务 torrent = server.add_torrent( content=content, - download_dir=download_dir.as_posix(), + download_dir=self.normalize_path(download_dir, downloader), is_paused=is_paused, labels=labels, cookie=cookie diff --git a/app/schemas/system.py b/app/schemas/system.py index 0e943b8c..2b8e9222 100644 --- a/app/schemas/system.py +++ b/app/schemas/system.py @@ -51,6 +51,8 @@ class DownloaderConf(BaseModel): config: Optional[dict] = Field(default_factory=dict) # 是否启用 enabled: Optional[bool] = False + # 路径映射 + path_mapping: Optional[list[tuple[str, str]]] = Field(default_factory=list) class NotificationConf(BaseModel): From 6ebd50bebcc6aca395b5aa233cc4430a3256f5e1 Mon Sep 17 00:00:00 2001 From: stkevintan Date: Mon, 8 Dec 2025 16:30:40 +0800 Subject: [PATCH 2/4] update naming --- app/modules/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/modules/__init__.py b/app/modules/__init__.py index 2fef7dc9..f00ac949 100644 --- a/app/modules/__init__.py +++ b/app/modules/__init__.py @@ -296,20 +296,20 @@ class _DownloaderBase(ServiceBase[TService, DownloaderConf]): """ 根据下载器配置和路径映射,规范化下载路径 - :param path: 原始路径 + :param path: 存储路径 :param conf: 下载器配置 - :return: 规范化后的路径 + :return: 规范化后发送给下载器的路径 """ dir = path.as_posix() conf = self.get_config(downloader) if conf and conf.path_mapping: - for (src, dst) in conf.path_mapping: - src = Path(src.strip()).as_posix() - dst = Path(dst.strip()).as_posix() - if dir.startswith(src): - dir = dir.replace(src, dst, 1) + for (storage_path, download_path) in conf.path_mapping: + storage_path = Path(storage_path.strip()).as_posix() + download_path = Path(download_path.strip()).as_posix() + if dir.startswith(storage_path): + dir = dir.replace(storage_path, download_path, 1) break - # 处理存储协议前缀 + # 去掉存储协议前缀 if any, 下载器无法识别 for s in StorageSchema: prefix = f"{s.value}:" if dir.startswith(prefix): From 81f75485795975dcfd9c305618f0f7fb6236797a Mon Sep 17 00:00:00 2001 From: Kevin Tan Date: Mon, 8 Dec 2025 17:20:45 +0800 Subject: [PATCH 3/4] Update app/modules/__init__.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- app/modules/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/modules/__init__.py b/app/modules/__init__.py index f00ac949..0f0a5f19 100644 --- a/app/modules/__init__.py +++ b/app/modules/__init__.py @@ -307,8 +307,8 @@ class _DownloaderBase(ServiceBase[TService, DownloaderConf]): storage_path = Path(storage_path.strip()).as_posix() download_path = Path(download_path.strip()).as_posix() if dir.startswith(storage_path): - dir = dir.replace(storage_path, download_path, 1) - break + dir = dir.replace(storage_path, download_path, 1) + break # 去掉存储协议前缀 if any, 下载器无法识别 for s in StorageSchema: prefix = f"{s.value}:" From 99cab7c3375346def0e0b719263b92b2ad1ca312 Mon Sep 17 00:00:00 2001 From: Kevin Tan Date: Mon, 8 Dec 2025 17:21:33 +0800 Subject: [PATCH 4/4] Update app/modules/__init__.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- app/modules/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/modules/__init__.py b/app/modules/__init__.py index 0f0a5f19..97ce7b9c 100644 --- a/app/modules/__init__.py +++ b/app/modules/__init__.py @@ -297,7 +297,7 @@ class _DownloaderBase(ServiceBase[TService, DownloaderConf]): 根据下载器配置和路径映射,规范化下载路径 :param path: 存储路径 - :param conf: 下载器配置 + :param downloader: 下载器名称 :return: 规范化后发送给下载器的路径 """ dir = path.as_posix()