From 32afe6445f1f0dccac24c83995d0261601476d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=AF=E5=A4=A7=E4=BE=A0?= Date: Fri, 6 Feb 2026 17:10:59 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E6=95=B4=E7=90=86=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E7=BC=BA=E5=B0=91=E5=8E=86=E5=8F=B2=E8=AE=B0?= =?UTF-8?q?=E5=BD=95ID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/db/transferhistory_oper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/db/transferhistory_oper.py b/app/db/transferhistory_oper.py index 87c753a1..9d7c9387 100644 --- a/app/db/transferhistory_oper.py +++ b/app/db/transferhistory_oper.py @@ -125,7 +125,7 @@ class TransferHistoryOper(DbOper): """ 新增转移成功历史记录 """ - self.add_force( + return self.add_force( src=fileitem.path, src_storage=fileitem.storage, src_fileitem=fileitem.model_dump(), From 40395b29991602f800c433e5fe3eac0420b13de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=AF=E5=A4=A7=E4=BE=A0?= Date: Fri, 6 Feb 2026 17:05:12 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E5=9C=A8=E6=9E=84=E9=80=A0?= =?UTF-8?q?=E5=BE=85=E6=95=B4=E7=90=86=E6=96=87=E4=BB=B6=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=97=B6=E5=BC=95=E5=85=A5=E8=BF=87=E6=BB=A4=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E4=BB=A5=E7=AE=80=E5=8C=96=E5=90=8E=E7=BB=AD=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/chain/storage.py | 2 +- app/chain/transfer.py | 139 +++++++++++++++++++++++++-------------- app/schemas/exception.py | 7 ++ 3 files changed, 99 insertions(+), 49 deletions(-) diff --git a/app/chain/storage.py b/app/chain/storage.py index 42f42f15..304a9a38 100644 --- a/app/chain/storage.py +++ b/app/chain/storage.py @@ -156,7 +156,7 @@ class StorageChain(ChainBase): """ 判断是否包含蓝光必备的文件夹 """ - required_files = ("BDMV", "CERTIFICATE") + required_files = {"BDMV", "CERTIFICATE"} return any( item.type == "dir" and item.name in required_files for item in fileitems or [] diff --git a/app/chain/transfer.py b/app/chain/transfer.py index 0a42bfc1..5590da60 100755 --- a/app/chain/transfer.py +++ b/app/chain/transfer.py @@ -29,6 +29,7 @@ from app.log import logger from app.schemas import StorageOperSelectionEventData from app.schemas import TransferInfo, Notification, EpisodeFormat, FileItem, TransferDirectoryConf, \ TransferTask, TransferQueue, TransferJob, TransferJobTask +from app.schemas.exception import OperationInterrupted from app.schemas.types import TorrentStatus, EventType, MediaType, ProgressKey, NotificationType, MessageChannel, \ SystemConfigKey, ChainEventType, ContentType from app.utils.mixins import ConfigReloadMixin @@ -345,11 +346,13 @@ class JobManager: 检查指定种子的所有任务是否都已完成 """ with job_lock: - for job in self._job_view.values(): - for task in job.tasks: - if task.download_hash == download_hash: - if task.state not in ["completed", "failed"]: - return False + if any( + task.state not in {"completed", "failed"} + for job in self._job_view.values() + for task in job.tasks + if task.download_hash == download_hash + ): + return False return True def is_torrent_success(self, download_hash: str) -> bool: @@ -357,11 +360,13 @@ class JobManager: 检查指定种子的所有任务是否都已成功 """ with job_lock: - for job in self._job_view.values(): - for task in job.tasks: - if task.download_hash == download_hash: - if task.state not in ["completed"]: - return False + if any( + task.state != "completed" + for job in self._job_view.values() + for task in job.tasks + if task.download_hash == download_hash + ): + return False return True def has_tasks(self, meta: MetaBase, mediainfo: Optional[MediaInfo] = None, season: Optional[int] = None) -> bool: @@ -947,7 +952,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): # 如果未开启新增已入库媒体是否跟随TMDB信息变化则根据tmdbid查询之前的title if not settings.SCRAP_FOLLOW_TMDB: - transfer_history = transferhis.get_by_type_tmdbid(tmdbid=mediainfo.tmdb_id, + transfer_history = transferhis.get_by_type_tmdbid(tmdbid=mediainfo.tmdb_id, mtype=mediainfo.type.value) if transfer_history and mediainfo.title != transfer_history.title: mediainfo.title = transfer_history.title @@ -1169,14 +1174,29 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): return True def __get_trans_fileitems( - self, fileitem: FileItem, check: bool = True + self, + fileitem: FileItem, + predicate: Optional[Callable[[FileItem, bool], bool]], + verify_file_exists: bool = True, ) -> List[Tuple[FileItem, bool]]: """ - 获取整理目录或文件列表 + 获取待整理文件项列表 - :param fileitem: 文件项 - :param check: 检查文件是否存在,默认为True + :param fileitem: 源文件项 + :param predicate: 用于筛选目录或文件项 + 该函数接收两个参数: + + - `file_item`: 需要判断的文件项(类型为 `FileItem`) + - `is_bluray_dir`: 表示该项是否为蓝光原盘目录(布尔值) + + 函数应返回 `True` 表示保留该项,`False` 表示过滤掉 + + 若 `predicate` 为 `None`,则默认保留所有项 + :param verify_file_exists: 验证目录或文件是否存在,默认值为 `True` """ + if global_vars.is_system_stopped: + raise OperationInterrupted() + storagechain = StorageChain() def __is_bluray_sub(_path: str) -> bool: @@ -1194,7 +1214,12 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): return storagechain.get_file_item(storage=_storage, path=p.parent) return None - if check: + def _apply_predicate(file_item: FileItem, is_bluray_dir: bool) -> List[Tuple[FileItem, bool]]: + if predicate is None or predicate(file_item, is_bluray_dir): + return [(file_item, is_bluray_dir)] + return [] + + if verify_file_exists: latest_fileitem = storagechain.get_item(fileitem) if not latest_fileitem: logger.warn(f"目录或文件不存在:{fileitem.path}") @@ -1204,28 +1229,30 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): # 是否蓝光原盘子目录或文件 if __is_bluray_sub(fileitem.path): - if dir_item := __get_bluray_dir(fileitem.storage, Path(fileitem.path)): + if bluray_dir := __get_bluray_dir(fileitem.storage, Path(fileitem.path)): # 返回该文件所在的原盘根目录 - return [(dir_item, True)] + return _apply_predicate(bluray_dir, True) # 单文件 if fileitem.type == "file": - return [(fileitem, False)] + return _apply_predicate(fileitem, False) # 是否蓝光原盘根目录 sub_items = storagechain.list_files(fileitem, recursion=False) or [] if storagechain.contains_bluray_subdirectories(sub_items): # 当前目录是原盘根目录,不需要递归 - return [(fileitem, True)] + return _apply_predicate(fileitem, True) # 不是原盘根目录 递归获取目录内需要整理的文件项列表 return [ item for sub_item in sub_items for item in ( - self.__get_trans_fileitems(sub_item, check=False) + self.__get_trans_fileitems( + sub_item, predicate, verify_file_exists=False + ) if sub_item.type == "dir" - else [(sub_item, False)] + else _apply_predicate(sub_item, False) ) ] @@ -1275,22 +1302,47 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): transfer_exclude_words = SystemConfigOper().get(SystemConfigKey.TransferExcludeWords) # 汇总错误信息 err_msgs: List[str] = [] - # 递归获取待整理的文件/目录列表 - file_items = self.__get_trans_fileitems(fileitem) - if not file_items: - logger.warn(f"{fileitem.path} 没有找到可整理的媒体文件") - return False, f"{fileitem.name} 没有找到可整理的媒体文件" + def _filter(file_item: FileItem, is_bluray_dir: bool) -> bool: + """ + 过滤文件项 - # 有集自定义格式,过滤文件 - if formaterHandler: - file_items = [f for f in file_items if formaterHandler.match(f[0].name)] + :return: True 表示保留,False 表示排除 + """ + if continue_callback and not continue_callback(): + raise OperationInterrupted() + # 有集自定义格式,过滤文件 + if formaterHandler and not formaterHandler.match(file_item.name): + return False + # 过滤后缀和大小(蓝光目录、附加文件不过滤) + if ( + not is_bluray_dir + and not self.__is_subtitle_file(file_item) + and not self.__is_audio_file(file_item) + ): + if not self.__is_media_file(file_item): + return False + if not self.__is_allow_filesize(file_item, min_filesize): + return False + # 回收站及隐藏的文件不处理 + if ( + file_item.path.find("/@Recycle/") != -1 + or file_item.path.find("/#recycle/") != -1 + or file_item.path.find("/.") != -1 + or file_item.path.find("/@eaDir") != -1 + ): + logger.debug(f"{file_item.path} 是回收站或隐藏的文件") + return False + # 整理屏蔽词不处理 + if self._is_blocked_by_exclude_words(file_item.path, transfer_exclude_words): + return False + return True - # 过滤后缀和大小(蓝光目录、附加文件不过滤大小) - file_items = [f for f in file_items if f[1] or - self.__is_subtitle_file(f[0]) or - self.__is_audio_file(f[0]) or - (self.__is_media_file(f[0]) and self.__is_allow_filesize(f[0], min_filesize))] + try: + # 获取经过筛选后的待整理文件项列表 + file_items = self.__get_trans_fileitems(fileitem, predicate=_filter) + except OperationInterrupted: + return False, f"{fileitem.name} 已取消" if not file_items: logger.warn(f"{fileitem.path} 没有找到可整理的媒体文件") @@ -1303,21 +1355,10 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): try: for file_item, bluray_dir in file_items: if global_vars.is_system_stopped: - break + raise OperationInterrupted() if continue_callback and not continue_callback(): - break + raise OperationInterrupted() file_path = Path(file_item.path) - # 回收站及隐藏的文件不处理 - if file_item.path.find('/@Recycle/') != -1 \ - or file_item.path.find('/#recycle/') != -1 \ - or file_item.path.find('/.') != -1 \ - or file_item.path.find('/@eaDir') != -1: - logger.debug(f"{file_item.path} 是回收站或隐藏的文件") - continue - - # 整理屏蔽词不处理 - if self._is_blocked_by_exclude_words(file_item.path, transfer_exclude_words): - continue # 整理成功的不再处理 if not force: @@ -1415,6 +1456,8 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): transfer_tasks.append(transfer_task) else: logger.debug(f"{file_path.name} 已在整理列表中,跳过") + except OperationInterrupted: + return False, f"{fileitem.name} 已取消" finally: file_items.clear() del file_items diff --git a/app/schemas/exception.py b/app/schemas/exception.py index 655104b7..edc2e455 100644 --- a/app/schemas/exception.py +++ b/app/schemas/exception.py @@ -29,3 +29,10 @@ class RateLimitExceededException(LimitException): 这个异常通常用于本地限流逻辑(例如 RateLimiter),当系统检测到函数调用频率过高时,触发限流并抛出该异常。 """ pass + + +class OperationInterrupted(KeyboardInterrupt): + """ + 用于表示操作被中断 + """ + pass