From 44fa202778dea1740296ba5ce2b428e94070a02f Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 20 Jul 2026 16:16:25 +0800 Subject: [PATCH] fix: map Bangumi movie subjects correctly (#6155) --- app/chain/media.py | 14 +++- app/core/context.py | 23 +++++- tests/test_bangumi_media_type.py | 134 +++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 tests/test_bangumi_media_type.py diff --git a/app/chain/media.py b/app/chain/media.py index 5f98793b..9ff86ff3 100644 --- a/app/chain/media.py +++ b/app/chain/media.py @@ -845,7 +845,7 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): tmdbinfo = self._match_tmdb_with_names( meta_names=meta_names, year=year, - mtype=MediaType.TV, + mtype=MediaInfo.get_bangumi_media_type(bangumiinfo), season=meta.begin_season, ) return tmdbinfo @@ -885,7 +885,10 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): year = self._extract_year_from_bangumi(bangumiinfo) # 使用名称识别豆瓣媒体信息 return self.match_doubaninfo( - name=meta.name, year=year, mtype=MediaType.TV, season=meta.begin_season + name=meta.name, + year=year, + mtype=MediaInfo.get_bangumi_media_type(bangumiinfo), + season=meta.begin_season, ) return None @@ -1861,7 +1864,7 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): tmdbinfo = await self._async_match_tmdb_with_names( meta_names=meta_names, year=year, - mtype=MediaType.TV, + mtype=MediaInfo.get_bangumi_media_type(bangumiinfo), season=meta.begin_season, ) return tmdbinfo @@ -1901,6 +1904,9 @@ class MediaChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): year = self._extract_year_from_bangumi(bangumiinfo) # 使用名称识别豆瓣媒体信息 return await self.async_match_doubaninfo( - name=meta.name, year=year, mtype=MediaType.TV, season=meta.begin_season + name=meta.name, + year=year, + mtype=MediaInfo.get_bangumi_media_type(bangumiinfo), + season=meta.begin_season, ) return None diff --git a/app/core/context.py b/app/core/context.py index 8f31e334..82ae3c94 100644 --- a/app/core/context.py +++ b/app/core/context.py @@ -9,6 +9,8 @@ from app.core.metainfo import MetaInfo from app.schemas.types import MediaType from app.utils.string import StringUtils +BANGUMI_MOVIE_PLATFORMS = frozenset({"movie", "电影", "剧场版"}) + @dataclass class TorrentInfo: @@ -243,6 +245,10 @@ class SubtitleInfo: @dataclass class MediaInfo: + """ + 统一媒体信息,负责聚合各元数据源的标准字段 + """ + # 内部标记:是否命中本地识别缓存,不参与序列化 recognize_cache_hit = False # 来源:themoviedb、douban、bangumi @@ -721,7 +727,20 @@ class MediaInfo: elif type(current_value) is type(value): setattr(self, key, value) - def set_bangumi_info(self, info: dict): + @staticmethod + def get_bangumi_media_type(info: dict) -> MediaType: + """ + 根据Bangumi媒介平台获取标准媒体类型,未知平台兼容回退为电视剧 + + :param info: Bangumi条目信息 + :return: 标准媒体类型 + """ + platform = str(info.get("platform") or "").strip().casefold() + if platform in BANGUMI_MOVIE_PLATFORMS: + return MediaType.MOVIE + return MediaType.TV + + def set_bangumi_info(self, info: dict) -> None: """ 初始化Bangumi信息 """ @@ -735,7 +754,7 @@ class MediaInfo: self.bangumi_id = info.get("id") # 类型 if not self.type: - self.type = MediaType.TV + self.type = self.get_bangumi_media_type(info) # 标题 if not self.title: self.title = info.get("name_cn") or info.get("name") diff --git a/tests/test_bangumi_media_type.py b/tests/test_bangumi_media_type.py new file mode 100644 index 00000000..5f313b54 --- /dev/null +++ b/tests/test_bangumi_media_type.py @@ -0,0 +1,134 @@ +import asyncio +from typing import Optional + +import pytest + +from app.chain.media import MediaChain +from app.core.context import MediaInfo +from app.schemas.types import MediaType + + +@pytest.mark.parametrize( + ("platform", "expected_type"), + [ + ("剧场版", MediaType.MOVIE), + ("Movie", MediaType.MOVIE), + ("电影", MediaType.MOVIE), + ("TV", MediaType.TV), + ("WEB", MediaType.TV), + ("OVA", MediaType.TV), + ("未知", MediaType.TV), + (None, MediaType.TV), + ], +) +def test_bangumi_platform_maps_to_media_type( + platform: Optional[str], expected_type: MediaType +) -> None: + """Bangumi媒介平台应映射为正确的标准媒体类型。""" + media_info = MediaInfo(bangumi_info={"id": 1, "name": "测试条目", "platform": platform}) + + assert media_info.type == expected_type + + +def test_bangumi_media_type_does_not_override_explicit_type() -> None: + """显式媒体类型应优先于Bangumi媒介平台。""" + media_info = MediaInfo( + type=MediaType.TV, + bangumi_info={"id": 1, "name": "测试条目", "platform": "剧场版"}, + ) + + assert media_info.type == MediaType.TV + + +class _SyncBangumiMediaChain: + """同步Bangumi跨数据源转换测试桩。""" + + def __init__(self): + """初始化调用参数记录。""" + self.tmdb_mtype = None + self.douban_mtype = None + + def bangumi_info(self, bangumiid: int) -> dict: + """返回剧场版Bangumi条目信息。""" + return { + "id": bangumiid, + "name": "Movie Test", + "name_cn": "电影测试", + "date": "2026-01-01", + "platform": "剧场版", + } + + @staticmethod + def _extract_year_from_bangumi(bangumi_info: dict) -> str: + """返回测试条目的年份。""" + return bangumi_info["date"][:4] + + def _match_tmdb_with_names(self, **kwargs) -> dict: + """记录TMDB匹配使用的媒体类型。""" + self.tmdb_mtype = kwargs["mtype"] + return {"id": 100} + + def match_doubaninfo(self, **kwargs) -> dict: + """记录豆瓣匹配使用的媒体类型。""" + self.douban_mtype = kwargs["mtype"] + return {"id": "200"} + + +def test_bangumi_movie_conversion_uses_movie_type() -> None: + """Bangumi剧场版转TMDB和豆瓣时均应按电影匹配。""" + chain = _SyncBangumiMediaChain() + + tmdb_info = MediaChain.get_tmdbinfo_by_bangumiid(chain, 1) + douban_info = MediaChain.get_doubaninfo_by_bangumiid(chain, 1) + + assert tmdb_info == {"id": 100} + assert douban_info == {"id": "200"} + assert chain.tmdb_mtype == MediaType.MOVIE + assert chain.douban_mtype == MediaType.MOVIE + + +class _AsyncBangumiMediaChain: + """异步Bangumi跨数据源转换测试桩。""" + + def __init__(self): + """初始化调用参数记录。""" + self.tmdb_mtype = None + self.douban_mtype = None + + async def async_bangumi_info(self, bangumiid: int) -> dict: + """返回剧场版Bangumi条目信息。""" + return { + "id": bangumiid, + "name": "Movie Test", + "name_cn": "电影测试", + "date": "2026-01-01", + "platform": "Movie", + } + + @staticmethod + def _extract_year_from_bangumi(bangumi_info: dict) -> str: + """返回测试条目的年份。""" + return bangumi_info["date"][:4] + + async def _async_match_tmdb_with_names(self, **kwargs) -> dict: + """记录异步TMDB匹配使用的媒体类型。""" + self.tmdb_mtype = kwargs["mtype"] + return {"id": 100} + + async def async_match_doubaninfo(self, **kwargs) -> dict: + """记录异步豆瓣匹配使用的媒体类型。""" + self.douban_mtype = kwargs["mtype"] + return {"id": "200"} + + +def test_async_bangumi_movie_conversion_uses_movie_type() -> None: + """异步Bangumi电影转换应向TMDB和豆瓣传递电影类型。""" + chain = _AsyncBangumiMediaChain() + + tmdb_info = asyncio.run(MediaChain.async_get_tmdbinfo_by_bangumiid(chain, 1)) + douban_info = asyncio.run(MediaChain.async_get_doubaninfo_by_bangumiid(chain, 1)) + + assert tmdb_info == {"id": 100} + assert douban_info == {"id": "200"} + assert chain.tmdb_mtype == MediaType.MOVIE + assert chain.douban_mtype == MediaType.MOVIE