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/subscribe.py b/app/chain/subscribe.py index 56de48bc..bd02c189 100644 --- a/app/chain/subscribe.py +++ b/app/chain/subscribe.py @@ -1635,7 +1635,7 @@ class SubscribeChain(ChainBase): info = schemas.SubscribeEpisodeInfo() info.title = episode.name info.description = episode.overview - info.backdrop = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/w500${episode.still_path}" + info.backdrop = settings.TMDB_IMAGE_URL(episode.still_path, "w500") episodes[episode.episode_number] = info elif subscribe.type == MediaType.TV.value: # 根据开始结束集计算集信息 diff --git a/app/chain/transfer.py b/app/chain/transfer.py index 9b7ec94e..b5b9ab00 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() @@ -873,7 +873,7 @@ class TransferChain(ChainBase, metaclass=Singleton): state, errmsg = self.do_transfer( fileitem=FileItem( storage="local", - path=file_path.as_posix(), + path=file_path.as_posix() + ("/" if file_path.is_dir() else ""), type="dir" if not file_path.is_file() else "file", name=file_path.name, size=file_path.stat().st_size, @@ -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/core/config.py b/app/core/config.py index c1e58593..acfa5e7a 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -843,6 +843,18 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel): rename_format = re.sub(r'/+', '/', rename_format) return rename_format.strip("/") + def TMDB_IMAGE_URL(self, file_path: str, file_size: str = "original") -> str: + """ + 获取TMDB图片网址 + + :param file_path: TMDB API返回的xxx_path + :param file_size: 图片大小,例如:'original', 'w500' 等 + :return: 图片的完整URL + """ + return ( + f"https://{self.TMDB_IMAGE_DOMAIN}/t/p/{file_size}/{file_path.removeprefix('/')}" + ) + # 实例化配置 settings = Settings() diff --git a/app/core/context.py b/app/core/context.py index f2dd9053..dd52af63 100644 --- a/app/core/context.py +++ b/app/core/context.py @@ -479,11 +479,11 @@ class MediaInfo: self.episode_groups = info.pop("episode_groups").get("results") or [] # 海报 - if info.get('poster_path'): - self.poster_path = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{info.get('poster_path')}" + if path := info.get('poster_path'): + self.poster_path = settings.TMDB_IMAGE_URL(path) # 背景 - if info.get('backdrop_path'): - self.backdrop_path = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{info.get('backdrop_path')}" + if path := info.get('backdrop_path'): + self.backdrop_path = settings.TMDB_IMAGE_URL(path) # 导演和演员 self.directors, self.actors = __directors_actors(info) # 别名和译名 diff --git a/app/modules/filemanager/transhandler.py b/app/modules/filemanager/transhandler.py index 44b9e269..8d568624 100644 --- a/app/modules/filemanager/transhandler.py +++ b/app/modules/filemanager/transhandler.py @@ -132,6 +132,15 @@ class TransHandler: return self.result.model_copy() else: new_path = target_path / fileitem.name + # 在整理目录前先尝试获取原盘大小,避免整理记录出现0字节的情况 + # TODO 当前只计算STREAM目录内的文件大小,如果需要精确则递归完整目录 + if stream_fileitem := source_oper.get_item( + Path(fileitem.path) / "BDMV" / "STREAM" + ): + fileitem.size = 0 + files = source_oper.list(stream_fileitem) or [] + for file in files: + fileitem.size += file.size # 整理目录 new_diritem, errmsg = self.__transfer_dir(fileitem=fileitem, mediainfo=mediainfo, diff --git a/app/modules/themoviedb/__init__.py b/app/modules/themoviedb/__init__.py index de3fbcf2..5e5832d2 100644 --- a/app/modules/themoviedb/__init__.py +++ b/app/modules/themoviedb/__init__.py @@ -867,19 +867,19 @@ class TheMovieDbModule(_ModuleBase): backdrops = images.get("backdrops") if backdrops: backdrops = sorted(backdrops, key=lambda x: x.get("vote_average"), reverse=True) - mediainfo.backdrop_path = backdrops[0].get("file_path") + mediainfo.backdrop_path = settings.TMDB_IMAGE_URL(backdrops[0].get("file_path")) # 标志 if not mediainfo.logo_path: logos = images.get("logos") if logos: logos = sorted(logos, key=lambda x: x.get("vote_average"), reverse=True) - mediainfo.logo_path = logos[0].get("file_path") + mediainfo.logo_path = settings.TMDB_IMAGE_URL(logos[0].get("file_path")) # 海报 if not mediainfo.poster_path: posters = images.get("posters") if posters: posters = sorted(posters, key=lambda x: x.get("vote_average"), reverse=True) - mediainfo.poster_path = posters[0].get("file_path") + mediainfo.poster_path = settings.TMDB_IMAGE_URL(posters[0].get("file_path")) return mediainfo def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]: @@ -957,7 +957,7 @@ class TheMovieDbModule(_ModuleBase): image_path = seasoninfo.get(image_type.value) if image_path: - return f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/{image_prefix}{image_path}" + return settings.TMDB_IMAGE_URL(image_path, image_prefix) return None def tmdb_movie_similar(self, tmdbid: int) -> List[MediaInfo]: diff --git a/app/modules/themoviedb/scraper.py b/app/modules/themoviedb/scraper.py index 47ad30d1..ef9ccafc 100644 --- a/app/modules/themoviedb/scraper.py +++ b/app/modules/themoviedb/scraper.py @@ -85,10 +85,10 @@ class TmdbScraper: seasoninfo = self.original_tmdb(mediainfo).get_tv_season_detail(mediainfo.tmdb_id, season) if seasoninfo: episodeinfo = self.__get_episode_detail(seasoninfo, episode) - if episodeinfo and episodeinfo.get("still_path"): + if still_path := episodeinfo.get("still_path"): # TMDB集still图片 still_name = f"{episode}" - still_url = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{episodeinfo.get('still_path')}" + still_url = settings.TMDB_IMAGE_URL(still_path) images[still_name] = still_url else: # 季的图片 @@ -115,7 +115,7 @@ class TmdbScraper: if _mediainfo: for attr_name, attr_value in _mediainfo.items(): if attr_name.endswith("_path") and attr_value is not None: - image_url = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{attr_value}" + image_url = settings.TMDB_IMAGE_URL(attr_value) image_name = attr_name.replace("_path", "") + Path(image_url).suffix images[image_name] = image_url return images @@ -127,11 +127,11 @@ class TmdbScraper: """ # TMDB季poster图片 sea_seq = str(season).rjust(2, '0') - if seasoninfo.get("poster_path"): + if poster_path := seasoninfo.get("poster_path"): # 后缀 - ext = Path(seasoninfo.get('poster_path')).suffix + ext = Path(poster_path).suffix # URL - url = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{seasoninfo.get('poster_path')}" + url = settings.TMDB_IMAGE_URL(poster_path) # S0海报格式不同 if season == 0: image_name = f"season-specials-poster{ext}" @@ -190,8 +190,8 @@ class TmdbScraper: DomUtils.add_node(doc, xactor, "type", "Actor") DomUtils.add_node(doc, xactor, "role", actor.get("character") or actor.get("role") or "") DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "") - DomUtils.add_node(doc, xactor, "thumb", - f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{actor.get('profile_path')}") + if profile_path := actor.get('profile_path'): + DomUtils.add_node(doc, xactor, "thumb", settings.TMDB_IMAGE_URL(profile_path)) DomUtils.add_node(doc, xactor, "profile", f"https://www.themoviedb.org/person/{actor.get('id')}") # 风格 @@ -330,8 +330,8 @@ class TmdbScraper: DomUtils.add_node(doc, xactor, "name", actor.get("name") or "") DomUtils.add_node(doc, xactor, "type", "Actor") DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "") - DomUtils.add_node(doc, xactor, "thumb", - f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{actor.get('profile_path')}") + if profile_path := actor.get('profile_path'): + DomUtils.add_node(doc, xactor, "thumb", settings.TMDB_IMAGE_URL(profile_path)) DomUtils.add_node(doc, xactor, "profile", f"https://www.themoviedb.org/person/{actor.get('id')}") return doc diff --git a/app/modules/themoviedb/tmdb_cache.py b/app/modules/themoviedb/tmdb_cache.py index d753ced7..feb0c621 100644 --- a/app/modules/themoviedb/tmdb_cache.py +++ b/app/modules/themoviedb/tmdb_cache.py @@ -50,7 +50,7 @@ class TmdbCache(metaclass=WeakSingleton): """ 获取缓存KEY """ - return f"[{meta.type.value if meta.type else '未知'}]{meta.tmdbid or meta.name}-{meta.year}-{meta.begin_season}" + return f"[{meta.type.value if meta.type else '未知'}][{settings.TMDB_LOCALE}]{meta.tmdbid or meta.name}-{meta.year}-{meta.begin_season}" def get(self, meta: MetaBase): """ diff --git a/app/modules/themoviedb/tmdbapi.py b/app/modules/themoviedb/tmdbapi.py index 9503f166..417a5931 100644 --- a/app/modules/themoviedb/tmdbapi.py +++ b/app/modules/themoviedb/tmdbapi.py @@ -826,7 +826,7 @@ class TmdbApi: # 转换多语种标题 self.__update_tmdbinfo_extra_title(tmdb_info) # 转换中文标题 - if settings.TMDB_LOCALE == "zh": + if self.tmdb.language in ("zh", "zh-CN"): self.__update_tmdbinfo_cn_title(tmdb_info) return tmdb_info @@ -2134,7 +2134,7 @@ class TmdbApi: # 转换多语种标题 self.__update_tmdbinfo_extra_title(tmdb_info) # 转换中文标题 - if settings.TMDB_LOCALE == "zh": + if self.tmdb.language in ("zh", "zh-CN"): self.__update_tmdbinfo_cn_title(tmdb_info) return tmdb_info diff --git a/app/monitor.py b/app/monitor.py index e0a95970..b7ef3a72 100644 --- a/app/monitor.py +++ b/app/monitor.py @@ -695,11 +695,13 @@ class Monitor(ConfigReloadMixin, metaclass=SingletonClass): # 全程加锁 with lock: + is_bluray_folder = False # 蓝光原盘文件处理 if __is_bluray_sub(event_path): event_path = __get_bluray_dir(event_path) if not event_path: return + is_bluray_folder = True # TTL缓存控重 if self._cache.get(str(event_path)): @@ -708,13 +710,20 @@ class Monitor(ConfigReloadMixin, metaclass=SingletonClass): self._cache[str(event_path)] = True try: - logger.info(f"开始整理文件: {event_path}") + if is_bluray_folder: + logger.info(f"开始整理蓝光原盘: {event_path}") + else: + logger.info(f"开始整理文件: {event_path}") # 开始整理 TransferChain().do_transfer( fileitem=FileItem( storage=storage, - path=event_path.as_posix(), - type="file", + path=( + event_path.as_posix() + if not is_bluray_folder + else event_path.as_posix() + "/" + ), + type="file" if not is_bluray_folder else "dir", name=event_path.name, basename=event_path.stem, extension=event_path.suffix[1:], 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..5f8f9a7e --- /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[tuple[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)