diff --git a/app/chain/media.py b/app/chain/media.py index 40a8824c..daef3013 100644 --- a/app/chain/media.py +++ b/app/chain/media.py @@ -315,21 +315,6 @@ class MediaChain(ChainBase): ) return None - @staticmethod - def is_bluray_folder(fileitem: schemas.FileItem) -> bool: - """ - 判断是否为原盘目录 - """ - if not fileitem or fileitem.type != "dir": - return False - # 蓝光原盘目录必备的文件或文件夹 - required_files = ['BDMV', 'CERTIFICATE'] - # 检查目录下是否存在所需文件或文件夹 - for item in StorageChain().list_files(fileitem): - if item.name in required_files: - return True - return False - @eventmanager.register(EventType.MetadataScrape) def scrape_metadata_event(self, event: Event): """ @@ -370,7 +355,7 @@ class MediaChain(ChainBase): else: if file_list: # 如果是BDMV原盘目录,只对根目录进行刮削,不处理子目录 - if self.is_bluray_folder(fileitem): + if storagechain.is_bluray_folder(fileitem): logger.info(f"检测到BDMV原盘目录,只对根目录进行刮削:{fileitem.path}") self.scrape_metadata(fileitem=fileitem, mediainfo=mediainfo, @@ -563,10 +548,23 @@ class MediaChain(ChainBase): logger.info("电影NFO刮削已关闭,跳过") else: # 电影目录 - if recursive: - # 处理文件 - if self.is_bluray_folder(fileitem): - # 原盘目录 + files = __list_files(_fileitem=fileitem) + is_bluray_folder = storagechain.contains_bluray_subdirectories(files) + if recursive and not is_bluray_folder: + # 处理非原盘目录内的文件 + for file in files: + if file.type == "dir": + # 电影不处理子目录 + continue + self.scrape_metadata(fileitem=file, + mediainfo=mediainfo, + init_folder=False, + parent=fileitem, + overwrite=overwrite) + # 生成目录内图片文件 + if init_folder: + if is_bluray_folder: + # 检查电影NFO开关 if scraping_switchs.get('movie_nfo', True): nfo_path = filepath / (filepath.name + ".nfo") if overwrite or not storagechain.get_file_item(storage=fileitem.storage, path=nfo_path): @@ -581,20 +579,6 @@ class MediaChain(ChainBase): logger.info(f"已存在nfo文件:{nfo_path}") else: logger.info("电影NFO刮削已关闭,跳过") - else: - # 处理目录内的文件 - files = __list_files(_fileitem=fileitem) - for file in files: - if file.type == "dir": - # 电影不处理子目录 - continue - self.scrape_metadata(fileitem=file, - mediainfo=mediainfo, - init_folder=False, - parent=fileitem, - overwrite=overwrite) - # 生成目录内图片文件 - if init_folder: # 图片 image_dict = self.metadata_img(mediainfo=mediainfo) if image_dict: diff --git a/app/chain/storage.py b/app/chain/storage.py index ef0f760d..48fe8d8a 100644 --- a/app/chain/storage.py +++ b/app/chain/storage.py @@ -133,22 +133,29 @@ class StorageChain(ChainBase): """ return self.run_module("support_transtype", storage=storage) + def is_bluray_folder(self, fileitem: Optional[schemas.FileItem]) -> bool: + """ + 检查是否蓝光目录 + """ + if not fileitem or fileitem.type != "dir": + return False + return self.contains_bluray_subdirectories(self.list_files(fileitem)) + + @staticmethod + def contains_bluray_subdirectories(fileitems: Optional[List[schemas.FileItem]]) -> bool: + """ + 判断是否包含蓝光必备的文件夹 + """ + required_files = ("BDMV", "CERTIFICATE") + for item in fileitems or []: + if item.type == "dir" and item.name in required_files: + return True + return False + def delete_media_file(self, fileitem: schemas.FileItem, delete_self: bool = True) -> bool: """ 删除媒体文件,以及不含媒体文件的目录 """ - - def __is_bluray_dir(_fileitem: schemas.FileItem) -> bool: - """ - 检查是否蓝光目录 - """ - _dir_files = self.list_files(fileitem=_fileitem, recursion=False) - if _dir_files: - for _f in _dir_files: - if _f.type == "dir" and _f.name in ["BDMV", "CERTIFICATE"]: - return True - return False - media_exts = settings.RMT_MEDIAEXT + settings.DOWNLOAD_TMPEXT fileitem_path = Path(fileitem.path) if fileitem.path else Path("") if len(fileitem_path.parts) <= 2: @@ -156,7 +163,7 @@ class StorageChain(ChainBase): return False if fileitem.type == "dir": # 本身是目录 - if __is_bluray_dir(fileitem): + if self.is_bluray_folder(fileitem): logger.warn(f"正在删除蓝光原盘目录:【{fileitem.storage}】{fileitem.path}") if not self.delete_file(fileitem): logger.warn(f"【{fileitem.storage}】{fileitem.path} 删除失败") diff --git a/app/chain/transfer.py b/app/chain/transfer.py index 9b7ec94e..d6a0f588 100755 --- a/app/chain/transfer.py +++ b/app/chain/transfer.py @@ -376,7 +376,7 @@ class TransferChain(ChainBase, metaclass=Singleton): self._transfer_interval = 15 # 事件管理器 self.jobview = JobManager() - # 车移成功的文件清单 + # 转移成功的文件清单 self._success_target_files: Dict[str, List[str]] = {} # 启动整理任务 self.__init() @@ -908,16 +908,6 @@ class TransferChain(ChainBase, metaclass=Singleton): """ storagechain = StorageChain() - def __contains_bluray_sub(_fileitems: List[FileItem]) -> bool: - """ - 判断是否包含蓝光子目录 - """ - if _fileitems: - for sub in _fileitems: - if sub.type == "dir" and sub.name in ["BDMV", "CERTIFICATE"]: - return True - return False - def __is_bluray_sub(_path: str) -> bool: """ 判断是否蓝光原盘目录内的子目录或文件 @@ -949,7 +939,7 @@ class TransferChain(ChainBase, metaclass=Singleton): # 蓝光原盘根目录 sub_items = storagechain.list_files(fileitem) or [] - if __contains_bluray_sub(sub_items): + if storagechain.contains_bluray_subdirectories(sub_items): return [(fileitem, True)] # 需要整理的文件项列表 diff --git a/app/utils/system.py b/app/utils/system.py index 19ecbe4d..13f848cc 100644 --- a/app/utils/system.py +++ b/app/utils/system.py @@ -479,6 +479,8 @@ class SystemUtils: def is_bluray_dir(dir_path: Path) -> bool: """ 判断是否为蓝光原盘目录 + + (该方法已弃用,改用`StorageChain().is_bluray_folder)` """ if not dir_path.is_dir(): return False diff --git a/tests/cases/files.py b/tests/cases/files.py index 5781898e..9f2ec84b 100644 --- a/tests/cases/files.py +++ b/tests/cases/files.py @@ -1,161 +1,99 @@ #!/usr/bin/env python # -*- coding:utf-8 -*- +# 文件列表结构 list[tuple(名称, 子文件列表 或 文件大小)] bluray_files = [ - { - "name": "FOLDER", - "children": [ - { - "name": "Digimon", - "children": [ - { - "name": "Digimon (2055)", - "children": [ - { - "name": "BDMV", - "children": [ - { - "name": "STREAM", - "children": [ - { - "name": "00000.m2ts", - "size": 104857600, - }, - { - "name": "00001.m2ts", - "size": 104857600, - }, + ( + "FOLDER", + [ + ( + "Digimon", + [ + ( + "Digimon BluRay (2055)", + [ + ( + "BDMV", + [ + ( + "STREAM", + [ + ("00000.m2ts", 104857600), + ("00001.m2ts", 104857600), ], - }, + ), ], - }, - { - "name": "CERTIFICATE", - "children": [], - }, + ), + ("CERTIFICATE", None), ], - }, - { - "name": "Digimon (2099)", - "children": [ - { - "name": "BDMV", - "children": [ - { - "name": "STREAM", - "children": [ - { - "name": "00000.m2ts", - "size": 104857600, - }, - { - "name": "00001.m2ts", - "size": 104857600, - }, - { - "name": "00002.m2ts.!qB", - "size": 104857600, - }, + ), + ( + "Digimon BluRay (2099)", + [ + ( + "BDMV", + [ + ( + "STREAM", + [ + ("00000.m2ts", 104857600), + ("00001.m2ts", 104857600), + ("00002.m2ts.!qB", 104857600), ], - }, + ), ], - }, - { - "name": "CERTIFICATE", - "children": [], - }, + ), + ("CERTIFICATE", None), ], - }, - { - "name": "Digimon (2199)", - "children": [ - { - "name": "Digimon.2199.mp4", - "size": 104857600, - }, - ], - }, + ), + ("Digimon (2199)", [("Digimon.2199.mp4", 104857600)]), ], - }, - { - "name": "Pokemon (2016)", - "children": [ - { - "name": "BDMV", - "children": [ - { - "name": "STREAM", - "children": [ - { - "name": "00000.m2ts", - "size": 104857600, - }, - { - "name": "00001.m2ts", - "size": 104857600, - }, + ), + ( + "Pokemon BluRay (2016)", + [ + ( + "BDMV", + [ + ( + "STREAM", + [ + ("00000.m2ts", 104857600), + ("00001.m2ts", 104857600), ], - }, + ) ], - }, - { - "name": "CERTIFICATE", - "children": [], - }, + ), + ("CERTIFICATE", None), ], - }, - { - "name": "Pokemon (2021)", - "children": [ - { - "name": "BDMV", - "children": [ - { - "name": "STREAM", - "children": [ - { - "name": "00000.m2ts", - "size": 104857600, - }, - { - "name": "00001.m2ts", - "size": 104857600, - }, + ), + ( + "Pokemon BluRay (2021)", + [ + ( + "BDMV", + [ + ( + "STREAM", + [ + ("00000.m2ts", 104857600), + ("00001.m2ts", 104857600), ], - }, + ) ], - }, - { - "name": "CERTIFICATE", - "children": [], - }, + ), + ("CERTIFICATE", None), ], - }, - { - "name": "Pokemon (2028)", - "children": [ - { - "name": "Pokemon.2028.mkv", - "size": 104857600, - }, - { - "name": "Pokemon.2028.hdr.mkv.!qB", - "size": 104857600, - }, + ), + ( + "Pokemon (2028)", + [ + ("Pokemon.2028.mkv", 104857600), + ("Pokemon.2028.hdr.mkv.!qB", 104857600), ], - }, - { - "name": "Pokemon.2029.mp4", - "size": 104857600, - }, - { - "name": "Pokemon (2030)", - "children": [ - { - "name": "S", - "size": 104857600, - }, - ], - }, + ), + ("Pokemon.2029.mp4", 104857600), + ("Pokemon.2039.mp4", 104857600), + ("Pokemon (2030)", [("S", 104857600)]), ], - }, + ) ] diff --git a/tests/run.py b/tests/run.py index 7daf3882..40d2c79f 100644 --- a/tests/run.py +++ b/tests/run.py @@ -1,5 +1,6 @@ import unittest +from tests.test_bluray import BluRayTest from tests.test_metainfo import MetaInfoTest from tests.test_object import ObjectUtilsTest @@ -12,6 +13,9 @@ if __name__ == '__main__': suite.addTest(MetaInfoTest('test_emby_format_ids')) suite.addTest(ObjectUtilsTest('test_check_method')) + # 测试蓝光目录识别 + suite.addTest(BluRayTest()) + # 运行测试 runner = unittest.TextTestRunner() runner.run(suite) diff --git a/tests/test_bluray.py b/tests/test_bluray.py new file mode 100644 index 00000000..85e5aff6 --- /dev/null +++ b/tests/test_bluray.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +from pathlib import Path +from typing import Optional +from unittest import TestCase +from unittest.mock import patch + +from app import schemas +from app.chain.media import MediaChain +from app.chain.storage import StorageChain +from app.chain.transfer import TransferChain +from app.core.context import MediaInfo +from app.core.event import Event +from app.core.metainfo import MetaInfoPath +from app.db.models.transferhistory import TransferHistory +from app.log import logger +from app.schemas.types import EventType +from tests.cases.files import bluray_files + + +class BluRayTest(TestCase): + def __init__(self, methodName="test"): + super().__init__(methodName) + self.__history = [] + self.__root = schemas.FileItem( + path="/", name="", type="dir", extension="", size=0 + ) + self.__all = {self.__root.path: self.__root} + + def __build_child(parent: schemas.FileItem, files: list[(str, list | int)]): + parent.children = [] + for name, children in files: + sep = "" if parent.path.endswith("/") else "/" + file_item = schemas.FileItem( + path=f"{parent.path}{sep}{name}", + name=name, + extension=Path(name).suffix[1:], + basename=Path(name).stem, + type="file" if isinstance(children, int) else "dir", + size=children if isinstance(children, int) else 0, + ) + parent.children.append(file_item) + self.__all[file_item.path] = file_item + if isinstance(children, list): + __build_child(file_item, children) + + __build_child(self.__root, bluray_files) + + def _test_do_transfer(self): + def __test_do_transfer(path: str): + self.__history.clear() + TransferChain().do_transfer( + force=False, + background=False, + fileitem=StorageChain().get_file_item(None, Path(path)), + ) + return self.__history + + self.assertEqual( + [ + "/FOLDER/Digimon/Digimon BluRay (2055)", + "/FOLDER/Digimon/Digimon BluRay (2099)", + "/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4", + ], + __test_do_transfer("/FOLDER/Digimon"), + ) + + self.assertEqual( + [ + "/FOLDER/Digimon/Digimon BluRay (2055)", + ], + __test_do_transfer("/FOLDER/Digimon/Digimon BluRay (2055)"), + ) + + self.assertEqual( + [ + "/FOLDER/Digimon/Digimon BluRay (2055)", + ], + __test_do_transfer("/FOLDER/Digimon/Digimon BluRay (2055)/BDMV"), + ) + + self.assertEqual( + [ + "/FOLDER/Digimon/Digimon BluRay (2055)", + ], + __test_do_transfer("/FOLDER/Digimon/Digimon BluRay (2055)/BDMV/STREAM"), + ) + + self.assertEqual( + [ + "/FOLDER/Digimon/Digimon BluRay (2055)", + ], + __test_do_transfer( + "/FOLDER/Digimon/Digimon BluRay (2055)/BDMV/STREAM/00001.m2ts" + ), + ) + + self.assertEqual( + [ + "/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4", + ], + __test_do_transfer("/FOLDER/Digimon/Digimon (2199)"), + ) + + self.assertEqual( + [ + "/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4", + ], + __test_do_transfer("/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4"), + ) + + self.assertEqual( + [ + "/FOLDER/Pokemon.2029.mp4", + ], + __test_do_transfer("/FOLDER/Pokemon.2029.mp4"), + ) + + self.assertEqual( + [ + "/FOLDER/Digimon/Digimon BluRay (2055)", + "/FOLDER/Digimon/Digimon BluRay (2099)", + "/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4", + "/FOLDER/Pokemon BluRay (2016)", + "/FOLDER/Pokemon BluRay (2021)", + "/FOLDER/Pokemon (2028)/Pokemon.2028.mkv", + "/FOLDER/Pokemon.2029.mp4", + "/FOLDER/Pokemon.2039.mp4", + ], + __test_do_transfer("/FOLDER"), + ) + + def _test_scrape_metadata(self, mock_metadata_nfo): + def __test_scrape_metadata(path: str, excepted_nfo_count: int = 1): + """ + 分别测试手动和自动刮削 + """ + fileitem = StorageChain().get_file_item(None, Path(path)) + meta = MetaInfoPath(Path(fileitem.path)) + mediainfo = MediaInfo(tmdb_info={"id": 1, "title": "Test"}) + + # 测试手动刮削 + logger.debug(f"测试手动刮削 {path}") + mock_metadata_nfo.call_count = 0 + MediaChain().scrape_metadata( + fileitem=fileitem, meta=meta, mediainfo=mediainfo, overwrite=True + ) + # 确保调用了指定次数的metadata_nfo + self.assertEqual(mock_metadata_nfo.call_count, excepted_nfo_count) + + # 测试自动刮削 + logger.debug(f"测试自动刮削 {path}") + mock_metadata_nfo.call_count = 0 + MediaChain().scrape_metadata_event( + Event( + event_type=EventType.MetadataScrape, + event_data={ + "meta": meta, + "mediainfo": mediainfo, + "fileitem": fileitem, + "file_list": [fileitem.path], + "overwrite": False, + }, + ) + ) + # 调用了指定次数的metadata_nfo + self.assertEqual(mock_metadata_nfo.call_count, excepted_nfo_count) + + # 刮削原盘目录 + __test_scrape_metadata("/FOLDER/Digimon/Digimon BluRay (2099)") + # 刮削电影文件 + __test_scrape_metadata("/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4") + # 刮削电影目录 + __test_scrape_metadata("/FOLDER", excepted_nfo_count=2) + + @patch("app.chain.ChainBase.metadata_img", return_value=None) # 避免获取图片 + @patch("app.chain.ChainBase.__init__", return_value=None) # 避免不必要的模块初始化 + @patch("app.db.transferhistory_oper.TransferHistoryOper.get_by_src") + @patch("app.chain.storage.StorageChain.list_files") + @patch("app.chain.storage.StorageChain.get_parent_item") + @patch("app.chain.storage.StorageChain.get_file_item") + def test( + self, + mock_get_file_item, + mock_get_parent_item, + mock_list_files, + mock_get_by_src, + *_, + ): + def get_file_item(storage: str, path: Path): + path_posix = path.as_posix() + return self.__all.get(path_posix) + + def get_parent_item(fileitem: schemas.FileItem): + return get_file_item(None, Path(fileitem.path).parent) + + def list_files(fileitem: schemas.FileItem, recursion: bool = False): + if fileitem.type != "dir": + return None + if recursion: + result = [] + file_path = f"{fileitem.path}/" + for path, item in self.__all.items(): + if path.startswith(file_path): + result.append(item) + return result + else: + return fileitem.children + + def get_by_src(src: str, storage: Optional[str] = None): + self.__history.append(src) + result = TransferHistory() + result.status = True + return result + + mock_get_file_item.side_effect = get_file_item + mock_get_parent_item.side_effect = get_parent_item + mock_list_files.side_effect = list_files + mock_get_by_src.side_effect = get_by_src + + self._test_do_transfer() + + with patch( + "app.chain.media.MediaChain.metadata_nfo", return_value=None + ) as mock: + self._test_scrape_metadata(mock_metadata_nfo=mock)