diff --git a/app/chain/media.py b/app/chain/media.py index 2818e12d3..bb868e1a1 100644 --- a/app/chain/media.py +++ b/app/chain/media.py @@ -363,6 +363,15 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): if season_image_name := season_image_name_map.get(metadata_type): hint_ext = Path(filename_hint).suffix if filename_hint else ".jpg" final_filename = f"{season_image_name}{hint_ext}" + elif item_type == ScrapingTarget.MOVIE and current_fileitem.type == "file": + # 电影文件的图片应与视频文件同级保存,避免把图片路径拼到文件名下面。 + target_dir_item = parent_fileitem or self.storagechain.get_parent_item( + current_fileitem + ) + if not target_dir_item: + logger.error(f"无法获取文件 {current_fileitem.path} 的父目录项。") + return current_fileitem, None + target_dir_path = Path(target_dir_item.path) # 如果是 EPISODE 类型的图片(如thumb),通常也是放在文件同级目录,文件名与视频文件一致 elif ( metadata_type in [ScrapingMetadata.THUMB] @@ -390,6 +399,52 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): target_full_path = target_dir_path / final_filename return target_dir_item, target_full_path + def _get_target_fileitems_and_paths( + self, + current_fileitem: schemas.FileItem, + item_type: ScrapingTarget, + metadata_type: ScrapingMetadata, + filename_hint: Optional[str] = None, + parent_fileitem: Optional[schemas.FileItem] = None, + ) -> List[Tuple[schemas.FileItem, Path]]: + """ + 根据刮削上下文生成一个或多个保存目标。 + 季图片需要同时兼容根目录 seasonxx-poster 和季目录 poster 两种命名。 + """ + target_item, target_path = self._get_target_fileitem_and_path( + current_fileitem=current_fileitem, + item_type=item_type, + metadata_type=metadata_type, + filename_hint=filename_hint, + parent_fileitem=parent_fileitem, + ) + targets = [(target_item, target_path)] if target_path else [] + + if ( + item_type != ScrapingTarget.SEASON + or not filename_hint + or not filename_hint.lower().startswith("season") + or metadata_type not in { + ScrapingMetadata.POSTER, + ScrapingMetadata.BANNER, + ScrapingMetadata.THUMB, + } + ): + return targets + + season_parent_item = parent_fileitem or self.storagechain.get_parent_item( + current_fileitem + ) + if not season_parent_item: + logger.warn(f"无法获取季目录 {current_fileitem.path} 的父目录项,跳过根目录季图片") + return targets + + season_root_path = Path(current_fileitem.path).with_name(filename_hint) + root_target = (season_parent_item, season_root_path) + if root_target not in targets: + targets.insert(0, root_target) + return targets + def metadata_nfo( self, meta: MetaBase, @@ -773,7 +828,7 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): self.scrape_metadata( fileitem=fileitem, mediainfo=mediainfo, - init_folder=False, + init_folder=True, parent=self.storagechain.get_parent_item(fileitem), overwrite=overwrite, ) @@ -985,8 +1040,8 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): ) continue - # 获取目标 FileItem (`base_item`) 和 Path (`image_path`) - base_item, image_path = self._get_target_fileitem_and_path( + # 获取目标 FileItem 和 Path,季图片会同时写根目录和季目录。 + image_targets = self._get_target_fileitems_and_paths( current_fileitem=current_fileitem, item_type=item_type, metadata_type=metadata_type, @@ -994,19 +1049,20 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): parent_fileitem=parent_fileitem, ) - if not image_path: - continue + for base_item, image_path in image_targets: + if not image_path: + continue - # 文件存在检查 - file_exists = self.storagechain.get_file_item( - storage=base_item.storage, path=image_path - ) - - # 刮削决策 - if self._should_scrape(option, bool(file_exists), overwrite): - self._download_and_save_image( - fileitem=base_item, path=image_path, url=image_url + # 文件存在检查 + file_exists = self.storagechain.get_file_item( + storage=base_item.storage, path=image_path ) + + # 刮削决策 + if self._should_scrape(option, bool(file_exists), overwrite): + self._download_and_save_image( + fileitem=base_item, path=image_path, url=image_url + ) else: logger.debug( f"未找到图片类型 {image_name} 对应的 ScrapingMetadata,跳过。" @@ -1092,7 +1148,7 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): 处理电影刮削 """ if fileitem.type == "file": - # 电影文件:仅处理 NFO + # 电影文件始终处理 NFO,直接初始化文件时再补同级目录图片。 self._scrape_nfo_generic( current_fileitem=fileitem, meta=meta, @@ -1101,6 +1157,14 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): parent_fileitem=parent, overwrite=overwrite, ) + if init_folder: + self._scrape_images_generic( + current_fileitem=fileitem, + mediainfo=mediainfo, + item_type=ScrapingTarget.MOVIE, + parent_fileitem=parent, + overwrite=overwrite, + ) else: # 电影目录:递归处理文件并初始化目录 self._handle_movie_directory( diff --git a/app/modules/themoviedb/__init__.py b/app/modules/themoviedb/__init__.py index 716816503..3f870b7c3 100644 --- a/app/modules/themoviedb/__init__.py +++ b/app/modules/themoviedb/__init__.py @@ -969,7 +969,24 @@ class TheMovieDbModule(_ModuleBase): return None @staticmethod - def _process_tmdb_images(mediainfo: MediaInfo, images: dict) -> MediaInfo: + def _pick_best_tmdb_image(images: list) -> Optional[str]: + """ + 从 TMDB 图片候选中选出评分最高的文件路径。 + """ + if not images: + return None + images = sorted( + images, + key=lambda x: ( + x.get("vote_average") or 0, + x.get("vote_count") or 0, + ), + reverse=True, + ) + return images[0].get("file_path") + + @classmethod + def _process_tmdb_images(cls, mediainfo: MediaInfo, images: dict) -> MediaInfo: """ 处理 TMDB 图片数据 :param mediainfo: 媒体信息 @@ -980,22 +997,16 @@ class TheMovieDbModule(_ModuleBase): images = images[0] # 背景图 if not mediainfo.backdrop_path: - backdrops = images.get("backdrops") - if backdrops: - backdrops = sorted(backdrops, key=lambda x: x.get("vote_average"), reverse=True) - mediainfo.backdrop_path = settings.TMDB_IMAGE_URL(backdrops[0].get("file_path")) + if image_path := cls._pick_best_tmdb_image(images.get("backdrops")): + mediainfo.backdrop_path = settings.TMDB_IMAGE_URL(image_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 = settings.TMDB_IMAGE_URL(logos[0].get("file_path")) + if image_path := cls._pick_best_tmdb_image(images.get("logos")): + mediainfo.logo_path = settings.TMDB_IMAGE_URL(image_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 = settings.TMDB_IMAGE_URL(posters[0].get("file_path")) + if image_path := cls._pick_best_tmdb_image(images.get("posters")): + mediainfo.poster_path = settings.TMDB_IMAGE_URL(image_path) return mediainfo def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]: @@ -1011,9 +1022,15 @@ class TheMovieDbModule(_ModuleBase): # 调用TMDB图片接口 if mediainfo.type == MediaType.MOVIE: - images = self.tmdb.get_movie_images(mediainfo.tmdb_id) + images = self.tmdb.get_movie_images( + mediainfo.tmdb_id, + original_language=mediainfo.original_language, + ) else: - images = self.tmdb.get_tv_images(mediainfo.tmdb_id) + images = self.tmdb.get_tv_images( + mediainfo.tmdb_id, + original_language=mediainfo.original_language, + ) if not images: return mediainfo @@ -1033,9 +1050,15 @@ class TheMovieDbModule(_ModuleBase): # 调用TMDB图片接口 if mediainfo.type == MediaType.MOVIE: - images = await self.tmdb.async_get_movie_images(mediainfo.tmdb_id) + images = await self.tmdb.async_get_movie_images( + mediainfo.tmdb_id, + original_language=mediainfo.original_language, + ) else: - images = await self.tmdb.async_get_tv_images(mediainfo.tmdb_id) + images = await self.tmdb.async_get_tv_images( + mediainfo.tmdb_id, + original_language=mediainfo.original_language, + ) if not images: return mediainfo diff --git a/app/modules/themoviedb/scraper.py b/app/modules/themoviedb/scraper.py index 1996a953d..a83c2c271 100644 --- a/app/modules/themoviedb/scraper.py +++ b/app/modules/themoviedb/scraper.py @@ -127,6 +127,7 @@ class TmdbScraper: if poster_name and poster_url: images[poster_name] = poster_url else: + self.__ensure_main_images(mediainfo) # 获取媒体信息中原有图片 for attr_name, attr_value in vars(mediainfo).items(): if ( @@ -155,6 +156,52 @@ class TmdbScraper: images[image_name] = image_url return images + def __ensure_main_images(self, mediainfo: MediaInfo) -> None: + """ + 主媒体图片缺失时从 TMDB images 接口回填,避免当前语言没有图时只生成 NFO。 + """ + if not mediainfo or not mediainfo.tmdb_id: + return + if mediainfo.poster_path and mediainfo.backdrop_path: + return + if mediainfo.type == MediaType.MOVIE: + image_info = self.default_tmdb.get_movie_images( + mediainfo.tmdb_id, + original_language=mediainfo.original_language, + ) + else: + image_info = self.default_tmdb.get_tv_images( + mediainfo.tmdb_id, + original_language=mediainfo.original_language, + ) + if not image_info: + return + if not mediainfo.poster_path: + poster_path = self.__pick_best_image_path(image_info.get("posters")) + if poster_path: + mediainfo.poster_path = settings.TMDB_IMAGE_URL(poster_path) + if not mediainfo.backdrop_path: + backdrop_path = self.__pick_best_image_path(image_info.get("backdrops")) + if backdrop_path: + mediainfo.backdrop_path = settings.TMDB_IMAGE_URL(backdrop_path) + + @staticmethod + def __pick_best_image_path(images: list) -> Optional[str]: + """ + 从 TMDB 图片列表中选择评分和投票数最高的一张。 + """ + if not images: + return None + images = sorted( + images, + key=lambda item: ( + item.get("vote_average") or 0, + item.get("vote_count") or 0, + ), + reverse=True, + ) + return images[0].get("file_path") + @staticmethod def get_season_poster(seasoninfo: dict, season: int) -> Tuple[str, str]: """ diff --git a/app/modules/themoviedb/tmdbapi.py b/app/modules/themoviedb/tmdbapi.py index 3a26300d7..b4830676c 100644 --- a/app/modules/themoviedb/tmdbapi.py +++ b/app/modules/themoviedb/tmdbapi.py @@ -4,6 +4,7 @@ from typing import Optional, List import zhconv +from app.core.config import settings from app.log import logger from app.schemas.types import MediaType from app.utils.string import StringUtils @@ -1232,7 +1233,24 @@ class TmdbApi: logger.error(str(e)) return [] - def get_movie_images(self, tmdbid: int) -> dict: + @staticmethod + def _build_include_image_language(original_language: Optional[str] = None) -> str: + """ + 构造图片接口语言回退列表,避免当前语言没有图片时返回空列表。 + """ + languages = [] + for language in ( + getattr(settings, "TMDB_LOCALE", None), + "en", + None, + original_language, + ): + language = "null" if language is None else str(language).strip() + if language and language not in languages: + languages.append(language) + return ",".join(languages) + + def get_movie_images(self, tmdbid: int, original_language: Optional[str] = None) -> dict: """ 获取电影的图片 """ @@ -1240,12 +1258,17 @@ class TmdbApi: return {} try: logger.debug(f"正在获取电影图片:{tmdbid}...") - return self.movie.images(movie_id=tmdbid) or {} + return self.movie.images( + movie_id=tmdbid, + include_image_language=self._build_include_image_language( + original_language + ), + ) or {} except Exception as e: logger.error(str(e)) return {} - def get_tv_images(self, tmdbid: int) -> dict: + def get_tv_images(self, tmdbid: int, original_language: Optional[str] = None) -> dict: """ 获取电视剧的图片 """ @@ -1253,7 +1276,12 @@ class TmdbApi: return {} try: logger.debug(f"正在获取电视剧图片:{tmdbid}...") - return self.tv.images(tv_id=tmdbid) or {} + return self.tv.images( + tv_id=tmdbid, + include_image_language=self._build_include_image_language( + original_language + ), + ) or {} except Exception as e: logger.error(str(e)) return {} @@ -1965,7 +1993,9 @@ class TmdbApi: logger.error(str(e)) return [] - async def async_get_movie_images(self, tmdbid: int) -> dict: + async def async_get_movie_images( + self, tmdbid: int, original_language: Optional[str] = None + ) -> dict: """ 获取电影的图片(异步版本) """ @@ -1973,12 +2003,19 @@ class TmdbApi: return {} try: logger.debug(f"正在获取电影图片:{tmdbid}...") - return await self.movie.async_images(movie_id=tmdbid) or {} + return await self.movie.async_images( + movie_id=tmdbid, + include_image_language=self._build_include_image_language( + original_language + ), + ) or {} except Exception as e: logger.error(str(e)) return {} - async def async_get_tv_images(self, tmdbid: int) -> dict: + async def async_get_tv_images( + self, tmdbid: int, original_language: Optional[str] = None + ) -> dict: """ 获取电视剧的图片(异步版本) """ @@ -1986,7 +2023,12 @@ class TmdbApi: return {} try: logger.debug(f"正在获取电视剧图片:{tmdbid}...") - return await self.tv.async_images(tv_id=tmdbid) or {} + return await self.tv.async_images( + tv_id=tmdbid, + include_image_language=self._build_include_image_language( + original_language + ), + ) or {} except Exception as e: logger.error(str(e)) return {} diff --git a/tests/test_media_recognize_modules.py b/tests/test_media_recognize_modules.py index b69a64489..8e9fd3fd2 100644 --- a/tests/test_media_recognize_modules.py +++ b/tests/test_media_recognize_modules.py @@ -1,11 +1,13 @@ import asyncio from unittest import TestCase -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, patch from app.core.context import MediaInfo from app.core.meta import MetaBase from app.modules.douban import DoubanModule from app.modules.themoviedb import TheMovieDbModule +from app.modules.themoviedb.scraper import TmdbScraper +from app.modules.themoviedb.tmdbapi import TmdbApi from app.schemas.types import MediaType @@ -96,6 +98,68 @@ class MediaRecognizeModulesTest(TestCase): module._async_search_by_name.assert_called() module.tmdb.async_match_web.assert_not_called() + def test_tmdb_image_language_fallback_includes_current_en_null_and_original(self): + """TMDB 图片查询应带上语言回退,避免当前语言没有图片时直接返回空。""" + with patch("app.modules.themoviedb.tmdbapi.settings") as mock_settings: + mock_settings.TMDB_LOCALE = "zh" + + result = TmdbApi._build_include_image_language("ja") + + self.assertEqual(result, "zh,en,null,ja") + + def test_tmdb_obtain_images_uses_language_fallback_and_picks_best(self): + """obtain_images 应从图片接口回填缺失的海报和背景图。""" + module = TheMovieDbModule() + module.tmdb = Mock() + module.tmdb.get_movie_images.return_value = { + "posters": [ + {"file_path": "/low-poster.jpg", "vote_average": 2, "vote_count": 10}, + {"file_path": "/best-poster.jpg", "vote_average": 8, "vote_count": 1}, + ], + "backdrops": [ + {"file_path": "/best-backdrop.jpg", "vote_average": 7, "vote_count": 2}, + ], + } + mediainfo = MediaInfo( + tmdb_id=100, + type=MediaType.MOVIE, + original_language="ja", + ) + + result = module.obtain_images(mediainfo) + + self.assertIs(result, mediainfo) + module.tmdb.get_movie_images.assert_called_once_with(100, original_language="ja") + self.assertTrue(mediainfo.poster_path.endswith("/best-poster.jpg")) + self.assertTrue(mediainfo.backdrop_path.endswith("/best-backdrop.jpg")) + + def test_tmdb_scraper_metadata_img_fetches_missing_main_images(self): + """主媒体图片缺失时,刮削图片列表应先从 TMDB images 接口补齐。""" + scraper = TmdbScraper() + scraper._meta_tmdb = Mock() + scraper._meta_tmdb.get_movie_images.return_value = { + "posters": [ + {"file_path": "/fallback-poster.jpg", "vote_average": 5}, + ], + "backdrops": [ + {"file_path": "/fallback-backdrop.jpg", "vote_average": 4}, + ], + } + mediainfo = MediaInfo( + tmdb_id=200, + type=MediaType.MOVIE, + original_language="en", + ) + + images = scraper.get_metadata_img(mediainfo) + + scraper._meta_tmdb.get_movie_images.assert_called_once_with( + 200, + original_language="en", + ) + self.assertIn("poster.jpg", images) + self.assertIn("backdrop.jpg", images) + def test_douban_prepare_search_names_deduplicates_simplified_name(self): """豆瓣候选名称应保留顺序,并去掉繁简转换后的重复项。""" meta = MetaBase("流浪地球") diff --git a/tests/test_mediascrape.py b/tests/test_mediascrape.py index f37ca1dbf..5ef64736c 100644 --- a/tests/test_mediascrape.py +++ b/tests/test_mediascrape.py @@ -15,11 +15,20 @@ from app.core.metainfo import MetaInfo from app.schemas.types import EventType, MediaType, ScrapingTarget, ScrapingMetadata, ScrapingPolicy +def reset_media_chain_singleton(): + """清理 MediaChain 单例,避免测试间复用被 mock 的实例。""" + MediaChain._instances.pop((MediaChain, (), frozenset()), None) + + class TestMediaScrapingPaths(unittest.TestCase): def setUp(self): + reset_media_chain_singleton() self.media_chain = MediaChain() self.media_chain.storagechain = MagicMock() + def tearDown(self): + reset_media_chain_singleton() + def test_movie_file_nfo_path(self): fileitem = schemas.FileItem(path="/movies/avatar.mkv", name="avatar.mkv", type="file", storage="local") parent_item = schemas.FileItem(path="/movies", name="movies", type="dir", storage="local") @@ -75,6 +84,25 @@ class TestMediaScrapingPaths(unittest.TestCase): self.assertEqual(target_item, fileitem) self.assertEqual(target_path, Path("/tv/Show/Season 1/poster.jpg")) + def test_season_dir_poster_paths_include_root_and_season_dir(self): + """季海报应同时写剧集根目录和季目录,兼容不同媒体库。""" + parent_item = schemas.FileItem(path="/tv/Show", name="Show", type="dir", storage="local") + fileitem = schemas.FileItem(path="/tv/Show/Season 1", name="Season 1", type="dir", storage="local") + targets = self.media_chain._get_target_fileitems_and_paths( + current_fileitem=fileitem, + item_type=ScrapingTarget.SEASON, + metadata_type=ScrapingMetadata.POSTER, + filename_hint="season01-poster.jpg", + parent_fileitem=parent_item, + ) + self.assertEqual( + targets, + [ + (parent_item, Path("/tv/Show/season01-poster.jpg")), + (fileitem, Path("/tv/Show/Season 1/poster.jpg")), + ], + ) + def test_season_dir_specials_poster_path(self): fileitem = schemas.FileItem(path="/tv/Show/Specials", name="Specials", type="dir", storage="local") target_item, target_path = self.media_chain._get_target_fileitem_and_path( @@ -86,6 +114,20 @@ class TestMediaScrapingPaths(unittest.TestCase): self.assertEqual(target_item, fileitem) self.assertEqual(target_path, Path("/tv/Show/Specials/poster.jpg")) + def test_movie_file_image_path_uses_parent_dir(self): + """直接刮削电影文件时,图片应保存到父目录。""" + fileitem = schemas.FileItem(path="/movies/Avatar/Avatar.mkv", name="Avatar.mkv", type="file", storage="local") + parent_item = schemas.FileItem(path="/movies/Avatar", name="Avatar", type="dir", storage="local") + target_item, target_path = self.media_chain._get_target_fileitem_and_path( + current_fileitem=fileitem, + item_type=ScrapingTarget.MOVIE, + metadata_type=ScrapingMetadata.POSTER, + filename_hint="poster.jpg", + parent_fileitem=parent_item, + ) + self.assertEqual(target_item, parent_item) + self.assertEqual(target_path, Path("/movies/Avatar/poster.jpg")) + def test_episode_file_nfo_path(self): fileitem = schemas.FileItem(path="/tv/Show/Season 1/S01E01.mp4", name="S01E01.mp4", type="file", storage="local") parent_item = schemas.FileItem(path="/tv/Show/Season 1", name="Season 1", type="dir", storage="local") @@ -101,6 +143,7 @@ class TestMediaScrapingPaths(unittest.TestCase): class TestMediaScrapingNFO(unittest.TestCase): def setUp(self): + reset_media_chain_singleton() self.media_chain = MediaChain() self.media_chain.storagechain = MagicMock() self.media_chain.metadata_nfo = MagicMock(return_value="") @@ -111,6 +154,9 @@ class TestMediaScrapingNFO(unittest.TestCase): self.meta = MetaInfo("Avatar (2009)") self.mediainfo = MediaInfo() + def tearDown(self): + reset_media_chain_singleton() + def test_scrape_nfo_off(self): self.media_chain.scraping_policies.option.return_value = ScrapingOption("movie", "nfo", ScrapingPolicy.SKIP) self.media_chain._scrape_nfo_generic(self.fileitem, self.meta, self.mediainfo, ScrapingTarget.MOVIE) @@ -147,6 +193,7 @@ class TestMediaScrapingNFO(unittest.TestCase): class TestMediaScrapingImages(unittest.TestCase): def setUp(self): + reset_media_chain_singleton() self.media_chain = MediaChain() self.original_download = self.media_chain._download_and_save_image self.media_chain.storagechain = MagicMock() @@ -156,6 +203,7 @@ class TestMediaScrapingImages(unittest.TestCase): def tearDown(self): self.media_chain._download_and_save_image = self.original_download + reset_media_chain_singleton() def test_scrape_images_mapping(self): fileitem = schemas.FileItem(path="/movies/Avatar", name="Avatar", type="dir", storage="local") @@ -191,9 +239,43 @@ class TestMediaScrapingImages(unittest.TestCase): self.media_chain._scrape_images_generic(fileitem, mediainfo, ScrapingTarget.SEASON, season_number=1) calls = self.media_chain._download_and_save_image.call_args_list - self.assertEqual(len(calls), 1) - self.assertEqual(calls[0].kwargs["url"], "http://season01") - self.assertEqual(calls[0].kwargs["path"], Path("/tv/Show/Season 1/poster.jpg")) + self.assertEqual(len(calls), 2) + self.assertTrue(all(call.kwargs["url"] == "http://season01" for call in calls)) + self.assertEqual( + [call.kwargs["path"] for call in calls], + [ + Path("/tv/Show/season01-poster.jpg"), + Path("/tv/Show/Season 1/poster.jpg"), + ], + ) + + def test_scrape_movie_file_images_when_initialized_directly(self): + """直接初始化刮削电影文件时,应生成同级 poster/backdrop。""" + fileitem = schemas.FileItem(path="/movies/Avatar/Avatar.mkv", name="Avatar.mkv", type="file", storage="local") + parent_item = schemas.FileItem(path="/movies/Avatar", name="Avatar", type="dir", storage="local") + mediainfo = MediaInfo() + self.media_chain.metadata_img.return_value = { + "poster.jpg": "http://poster", + "backdrop.jpg": "http://backdrop", + } + self.media_chain.scraping_policies.option.return_value = ScrapingOption("movie", "poster", ScrapingPolicy.OVERWRITE) + self.media_chain.storagechain.get_file_item.return_value = None + + self.media_chain._scrape_images_generic( + fileitem, + mediainfo, + ScrapingTarget.MOVIE, + parent_fileitem=parent_item, + ) + + paths = [call.kwargs["path"] for call in self.media_chain._download_and_save_image.call_args_list] + self.assertEqual( + paths, + [ + Path("/movies/Avatar/poster.jpg"), + Path("/movies/Avatar/backdrop.jpg"), + ], + ) def test_scrape_episode_thumb_image_path(self): fileitem = schemas.FileItem(path="/tv/Show/Season 1/S01E01.mp4", name="S01E01.mp4", type="file", storage="local") @@ -295,11 +377,15 @@ class TestMediaScrapingImages(unittest.TestCase): class TestMediaScrapingTVDirectory(unittest.TestCase): def setUp(self): + reset_media_chain_singleton() self.media_chain = MediaChain() self.media_chain.storagechain = MagicMock() self.media_chain._scrape_nfo_generic = MagicMock() self.media_chain._scrape_images_generic = MagicMock() + def tearDown(self): + reset_media_chain_singleton() + @patch("app.chain.media.settings") def test_initialize_tv_directory_specials(self, mock_settings): # mock specials directory recognition @@ -366,9 +452,13 @@ class TestMediaScrapingTVDirectory(unittest.TestCase): class TestMediaScrapeEvents(unittest.TestCase): def setUp(self): + reset_media_chain_singleton() self.media_chain = MediaChain() self.media_chain.storagechain = MagicMock() + def tearDown(self): + reset_media_chain_singleton() + @patch("app.chain.media.MediaChain.scrape_metadata") def test_scrape_metadata_event_file( self, mock_scrape_metadata @@ -394,7 +484,7 @@ class TestMediaScrapeEvents(unittest.TestCase): mock_scrape_metadata.assert_called_once_with( fileitem=fileitem, mediainfo=mediainfo, - init_folder=False, + init_folder=True, parent=parent_item, overwrite=True )