From c6cfef198c07fba5c4e34e12c7fba256b756770f Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 20 Oct 2026 07:47:28 +0800 Subject: [PATCH] fix(douban): normalize person search image URLs --- app/modules/douban/__init__.py | 11 ++++- tests/test_douban_person_search.py | 70 ++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/test_douban_person_search.py diff --git a/app/modules/douban/__init__.py b/app/modules/douban/__init__.py index 7255dfc50..46f193a3d 100644 --- a/app/modules/douban/__init__.py +++ b/app/modules/douban/__init__.py @@ -1695,6 +1695,10 @@ class DoubanModule(MediaAuxiliaryProviderMixin, _ModuleBase): ) -> List[MediaPerson]: """ 把豆瓣人物搜索响应过滤并投影为统一人物列表。 + + :param name: 用于筛选人物标题的查询文本 + :param result: 豆瓣人物搜索响应 + :return: 图片尺寸已转换为 URL 的匹配人物列表 """ persons = [] for item in (result or {}).get("items") or []: @@ -1702,11 +1706,16 @@ class DoubanModule(MediaAuxiliaryProviderMixin, _ModuleBase): title = target.get("title") or "" if name not in title: continue + # 豆瓣尺寸图片可能包含宽高信息,统一模型只接收 URL;兼容已有字符串格式 + images = { + size: image.get("url") if isinstance(image, dict) else image + for size, image in (target.get("cover") or {}).items() + } persons.append(MediaPerson(source="douban", **{ "id": item.get("target_id"), "name": title, "url": target.get("url"), - "images": target.get("cover", {}), + "images": images, "avatar": (target.get("cover_img", {}).get("url") or "").replace( "/l/public/", "/s/public/" ), diff --git a/tests/test_douban_person_search.py b/tests/test_douban_person_search.py new file mode 100644 index 000000000..cc82d3db4 --- /dev/null +++ b/tests/test_douban_person_search.py @@ -0,0 +1,70 @@ +""" +豆瓣人物搜索图片投影的离线回归测试。 +""" + +import asyncio +from copy import deepcopy +from typing import Optional +from unittest.mock import AsyncMock, Mock + +import pytest + +from app.modules.douban import DoubanModule +from app.schemas.types import MediaSource + + +@pytest.mark.parametrize( + ("cover", "expected"), + [ + ( + {"large": {"url": "https://example.com/large.webp", "width": 0, "height": 0}, + "normal": {"url": "https://example.com/normal.jpg", "width": 0, "height": 0}}, + {"large": "https://example.com/large.webp", "normal": "https://example.com/normal.jpg"}, + ), + ({"large": "https://example.com/large.jpg"}, {"large": "https://example.com/large.jpg"}), + ( + {"large": {"url": "https://example.com/large.jpg"}, "normal": "https://example.com/normal.jpg"}, + {"large": "https://example.com/large.jpg", "normal": "https://example.com/normal.jpg"}, + ), + ({"large": {}, "normal": {"url": None}, "small": None}, {}), + ({}, {}), + (None, {}), + ], +) +def test_person_search_normalizes_images_in_sync_and_async_entries( + cover: Optional[dict[str, object]], expected: dict[str, str], +) -> None: + """ + 两个入口均应保留匹配人物及头像,并兼容嵌套、字符串和空图片。 + + :param cover: 豆瓣返回的尺寸图片数据 + :param expected: 标准人物模型中应保留的有效尺寸 URL + """ + response = {"items": [ + {"target_id": "100", "target": { + "title": "赵露思", "url": "https://example.com/person/100", + "cover": cover, "cover_img": {"url": "https://example.com/l/public/avatar.jpg"}, + }}, + {"target_id": "101", "target": {"title": "赵露思(演员)"}}, + {"target_id": "102", "target": {"title": "其他人物"}}, + ]} + original = deepcopy(response) + module = object.__new__(DoubanModule) + module.doubanapi = Mock() + module.doubanapi.person_search.return_value = response + module.doubanapi.async_person_search = AsyncMock(return_value=response) + + sync_result = module.search_persons("赵露思", media_source=MediaSource.Douban) + async_result = asyncio.run(module.async_search_persons("赵露思", media_source=MediaSource.Douban)) + + assert sync_result == async_result + assert [person.id for person in sync_result] == ["100", "101"] + assert sync_result[0].source == "douban" + assert sync_result[0].name == "赵露思" + assert sync_result[0].url == "https://example.com/person/100" + assert sync_result[0].avatar == "https://example.com/s/public/avatar.jpg" + assert sync_result[0].images.model_dump(exclude_none=True) == expected + assert sync_result[1].images.model_dump(exclude_none=True) == {} + assert response == original + module.doubanapi.person_search.assert_called_once_with(keyword="赵露思") + module.doubanapi.async_person_search.assert_awaited_once_with(keyword="赵露思")