"""Tests for Immich media source.""" from pathlib import Path import tempfile from unittest.mock import Mock, patch from aiohttp import web from aioimmich.assets.models import AssetType from aioimmich.exceptions import ImmichError, ImmichForbiddenError, ImmichNotFoundError import pytest from homeassistant.components.immich.const import DOMAIN from homeassistant.components.immich.media_source import ( ImmichMediaSource, ImmichMediaView, async_get_media_source, ) from homeassistant.components.media_player import ( BrowseError, BrowseMedia, MediaClass, SearchMediaQuery, ) from homeassistant.components.media_source import MediaSourceItem, Unresolvable from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.aiohttp import MockRequest, MockStreamReaderChunked from . import setup_integration from tests.common import MockConfigEntry async def test_get_media_source(hass: HomeAssistant) -> None: """Test the async_get_media_source.""" assert await async_setup_component(hass, "media_source", {}) source = await async_get_media_source(hass) assert isinstance(source, ImmichMediaSource) assert source.domain == DOMAIN @pytest.mark.parametrize( ("identifier", "exception_msg"), [ ("unique_id", "identifier_no_mime_type_unresolvable"), ( "unique_id|albums|album_id", "identifier_no_mime_type_unresolvable", ), ( "unique_id|albums|album_id|asset_id|filename", "identifier_unresolvable", ), ], ) async def test_resolve_media_bad_identifier( hass: HomeAssistant, identifier: str, exception_msg: str ) -> None: """Test resolve_media with bad identifiers.""" assert await async_setup_component(hass, "media_source", {}) source = await async_get_media_source(hass) item = MediaSourceItem(hass, DOMAIN, identifier, None) with pytest.raises(Unresolvable, match=exception_msg): await source.async_resolve_media(item) @pytest.mark.parametrize( ("identifier", "url", "mime_type"), [ ( "unique_id|albums|album_id|asset_id|filename.jpg|image/jpeg", "/immich/unique_id/asset_id/fullsize/image/jpeg", "image/jpeg", ), ( "unique_id|albums|album_id|asset_id|filename.png|image/png", "/immich/unique_id/asset_id/fullsize/image/png", "image/png", ), ( "unique_id|albums|album_id|asset_id|filename.mp4|video/mp4", "/immich/unique_id/asset_id/fullsize/video/mp4", "video/mp4", ), ], ) async def test_resolve_media_success( hass: HomeAssistant, identifier: str, url: str, mime_type: str ) -> None: """Test successful resolving an item.""" assert await async_setup_component(hass, "media_source", {}) source = await async_get_media_source(hass) item = MediaSourceItem(hass, DOMAIN, identifier, None) result = await source.async_resolve_media(item) assert result.url == url assert result.mime_type == mime_type async def test_browse_media_unconfigured(hass: HomeAssistant) -> None: """Test browse_media without any devices being configured.""" assert await async_setup_component(hass, "media_source", {}) source = await async_get_media_source(hass) item = MediaSourceItem( hass, DOMAIN, "unique_id/albums/album_id/asset_id/filename.png", None ) with pytest.raises(BrowseError, match="not_configured"): await source.async_browse_media(item) async def test_browse_media_get_root( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, ) -> None: """Test browse_media returning root media sources.""" assert await async_setup_component(hass, "media_source", {}) with patch("homeassistant.components.immich.PLATFORMS", []): await setup_integration(hass, mock_config_entry) source = await async_get_media_source(hass) # get root item = MediaSourceItem(hass, DOMAIN, "", None) root_media_source = await source.async_browse_media(item) assert root_media_source assert root_media_source.title == "Immich" assert root_media_source.can_search is False assert len(root_media_source.children) == 1 media_file = root_media_source.children[0] assert isinstance(media_file, BrowseMedia) assert media_file.title == "Someone" assert media_file.media_content_id == ( "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e" ) # get collections item = MediaSourceItem(hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", None) root_media_source = await source.async_browse_media(item) assert root_media_source assert root_media_source.title == "Someone" assert root_media_source.can_search is True assert len(root_media_source.children) == 4 media_file = root_media_source.children[0] assert isinstance(media_file, BrowseMedia) assert media_file.title == "albums" assert media_file.media_content_id == ( "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums" ) media_file = root_media_source.children[1] assert isinstance(media_file, BrowseMedia) assert media_file.title == "favorites" assert media_file.media_content_id == ( "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e|favorites|favorites" ) media_file = root_media_source.children[2] assert isinstance(media_file, BrowseMedia) assert media_file.title == "people" assert media_file.media_content_id == ( "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e|people" ) media_file = root_media_source.children[3] assert isinstance(media_file, BrowseMedia) assert media_file.title == "tags" assert media_file.media_content_id == ( "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e|tags" ) @pytest.mark.parametrize( ("collection", "children"), [ ( "albums", [{"title": "My Album", "asset_id": "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6"}], ), ( "people", [ {"title": "Me", "asset_id": "6176838a-ac5a-4d1f-9a35-91c591d962d8"}, {"title": "I", "asset_id": "3e66aa4a-a4a8-41a4-86fe-2ae5e490078f"}, {"title": "Myself", "asset_id": "a3c83297-684a-4576-82dc-b07432e8a18f"}, ], ), ( "tags", [ { "title": "Halloween", "asset_id": "67301cb8-cb73-4e8a-99e9-475cb3f7e7b5", }, { "title": "Holidays", "asset_id": "69bd487f-dc1e-4420-94c6-656f0515773d", }, ], ), ], ) async def test_browse_media_collections( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, collection: str, children: list[dict], ) -> None: """Test browse through collections.""" assert await async_setup_component(hass, "media_source", {}) with patch("homeassistant.components.immich.PLATFORMS", []): await setup_integration(hass, mock_config_entry) source = await async_get_media_source(hass) item = MediaSourceItem( hass, DOMAIN, f"{mock_config_entry.unique_id}|{collection}", None ) root_media_source = await source.async_browse_media(item) assert root_media_source assert root_media_source.title == collection assert root_media_source.can_search is True assert len(root_media_source.children) == len(children) for idx, child in enumerate(children): media_file = root_media_source.children[idx] assert isinstance(media_file, BrowseMedia) assert media_file.title == child["title"] assert media_file.media_content_id == ( "media-source://immich/" f"{mock_config_entry.unique_id}|{collection}|" f"{child['asset_id']}" ) @pytest.mark.parametrize( ("collection", "mocked_get_fn"), [ ("albums", ("albums", "async_get_all_albums")), ("favorites|favorites", ("search", "async_get_all_favorites")), ("people", ("people", "async_get_all_people")), ("tags", ("tags", "async_get_all_tags")), ], ) async def test_browse_media_collections_error( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, collection: str, mocked_get_fn: tuple[str, str], ) -> None: """Test browse_media with unknown collection.""" assert await async_setup_component(hass, "media_source", {}) with patch("homeassistant.components.immich.PLATFORMS", []): await setup_integration(hass, mock_config_entry) identifier = f"{mock_config_entry.unique_id}|{collection}" item = MediaSourceItem(hass, DOMAIN, identifier, None) source = await async_get_media_source(hass) # test generic ImmichError getattr( getattr(mock_immich, mocked_get_fn[0]), mocked_get_fn[1] ).side_effect = ImmichError( { "message": "Not found or no album.read access", "error": "Bad Request", "statusCode": 400, "correlationId": "e0hlizyl", } ) root_media_source = await source.async_browse_media(item) assert root_media_source assert root_media_source.identifier == identifier assert len(root_media_source.children) == 0 # test specific ImmichForbiddenError getattr( getattr(mock_immich, mocked_get_fn[0]), mocked_get_fn[1] ).side_effect = ImmichForbiddenError( { "message": "Missing required permission: asset.read", "error": "Forbidden", "statusCode": 403, "correlationId": "e0hlizyl", } ) with pytest.raises(BrowseError, match="Missing API permission"): await source.async_browse_media(item) @pytest.mark.parametrize( ("collection", "mocked_get_fn"), [ ("albums", ("search", "async_get_all_by_album_ids")), ("favorites", ("search", "async_get_all_favorites")), ("people", ("search", "async_get_all_by_person_ids")), ("tags", ("search", "async_get_all_by_tag_ids")), ], ) async def test_browse_media_collection_items_error( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, collection: str, mocked_get_fn: tuple[str, str], ) -> None: """Test browse_media returning albums.""" assert await async_setup_component(hass, "media_source", {}) with patch("homeassistant.components.immich.PLATFORMS", []): await setup_integration(hass, mock_config_entry) identifier = f"{mock_config_entry.unique_id}|{collection}|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6" item = MediaSourceItem( hass, DOMAIN, identifier, None, ) source = await async_get_media_source(hass) # test generic ImmichError getattr( getattr(mock_immich, mocked_get_fn[0]), mocked_get_fn[1] ).side_effect = ImmichError( { "message": "Not found or no album.read access", "error": "Bad Request", "statusCode": 400, "correlationId": "e0hlizyl", } ) root_media_source = await source.async_browse_media(item) assert root_media_source assert root_media_source.identifier == identifier assert len(root_media_source.children) == 0 # test specific ImmichForbiddenError getattr( getattr(mock_immich, mocked_get_fn[0]), mocked_get_fn[1] ).side_effect = ImmichForbiddenError( { "message": "Missing required permission: asset.read", "error": "Forbidden", "statusCode": 403, "correlationId": "e0hlizyl", } ) with pytest.raises(BrowseError, match="Missing API permission"): await source.async_browse_media(item) @pytest.mark.parametrize( ("collection", "collection_id", "title", "children"), [ ( "albums", "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", "My Album", [ { "original_file_name": "filename.jpg", "asset_id": "2e94c203-50aa-4ad2-8e29-56dd74e0eff4", "media_class": MediaClass.IMAGE, "media_content_type": "image/jpeg", "thumb_mime_type": "image/jpeg", "can_play": False, }, { "original_file_name": "filename.mp4", "asset_id": "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b", "media_class": MediaClass.VIDEO, "media_content_type": "video/mp4", "thumb_mime_type": "image/jpeg", "can_play": True, }, ], ), ( "favorites", "favorites", "favorites", [ { "original_file_name": "20260406_133809.jpg", "asset_id": "70af6d9d-097b-4b22-8684-dc2fe0d5e167", "media_class": MediaClass.IMAGE, "media_content_type": "image/jpeg", "thumb_mime_type": "image/jpeg", "can_play": False, }, { "original_file_name": "20260319_192209.jpg", "asset_id": "eee5aa96-0943-48e9-ae11-992216485c6d", "media_class": MediaClass.IMAGE, "media_content_type": "image/jpeg", "thumb_mime_type": "image/jpeg", "can_play": False, }, ], ), ( "people", "6176838a-ac5a-4d1f-9a35-91c591d962d8", "Me", [ { "original_file_name": "20250714_201122.jpg", "asset_id": "2242eda3-94c2-49ee-86d4-e9e071b6fbf4", "media_class": MediaClass.IMAGE, "media_content_type": "image/jpeg", "thumb_mime_type": "image/jpeg", "can_play": False, }, { "original_file_name": "20250714_201121.jpg", "asset_id": "046ac0d9-8acd-44d8-953f-ecb3c786358a", "media_class": MediaClass.IMAGE, "media_content_type": "image/jpeg", "thumb_mime_type": "image/jpeg", "can_play": False, }, ], ), ( "tags", "67301cb8-cb73-4e8a-99e9-475cb3f7e7b5", "Halloween", [ { "original_file_name": "20110306_025024.jpg", "asset_id": "ae3d82fc-beb5-4abc-ae83-11fcfa5e7629", "media_class": MediaClass.IMAGE, "media_content_type": "image/jpeg", "thumb_mime_type": "image/jpeg", "can_play": False, }, { "original_file_name": "20110306_024053.jpg", "asset_id": "b71d0d08-6727-44ae-8bba-83c190f95df4", "media_class": MediaClass.IMAGE, "media_content_type": "image/jpeg", "thumb_mime_type": "image/jpeg", "can_play": False, }, ], ), ], ) async def test_browse_media_collection_get_items( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, collection: str, collection_id: str, title: str, children: list[dict], ) -> None: """Test browse_media returning albums.""" assert await async_setup_component(hass, "media_source", {}) with patch("homeassistant.components.immich.PLATFORMS", []): await setup_integration(hass, mock_config_entry) source = await async_get_media_source(hass) item = MediaSourceItem( hass, DOMAIN, f"{mock_config_entry.unique_id}|{collection}|{collection_id}", None, ) root_media_source = await source.async_browse_media(item) assert root_media_source assert root_media_source.title == title assert len(root_media_source.children) == len(children) for idx, child in enumerate(children): media_file = root_media_source.children[idx] assert isinstance(media_file, BrowseMedia) assert media_file.identifier == ( f"{mock_config_entry.unique_id}|{collection}|{collection_id}|" f"{child['asset_id']}|{child['original_file_name']}|{child['media_content_type']}" ) assert media_file.title == child["original_file_name"] assert media_file.media_class == child["media_class"] assert media_file.media_content_type == child["media_content_type"] assert media_file.can_play is child["can_play"] assert not media_file.can_expand assert media_file.thumbnail == ( f"/immich/{mock_config_entry.unique_id}/" f"{child['asset_id']}/thumbnail/{child['thumb_mime_type']}" ) @pytest.mark.parametrize( ("collection", "mocked_get_fn"), [ pytest.param("albums", ("albums", "async_get_album_info"), id="albums"), pytest.param("people", ("people", "async_get_person_by_id"), id="people"), pytest.param("tags", ("tags", "async_get_tag_by_id"), id="tags"), ], ) async def test_browse_media_title_of_unknown_collection_item( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, collection: str, mocked_get_fn: tuple[str, str], ) -> None: """Test browse_media falls back to the collection name for unknown items.""" assert await async_setup_component(hass, "media_source", {}) with patch("homeassistant.components.immich.PLATFORMS", []): await setup_integration(hass, mock_config_entry) getattr( getattr(mock_immich, mocked_get_fn[0]), mocked_get_fn[1] ).side_effect = ImmichNotFoundError( { "message": "Not found or no permission", "error": "Bad Request", "statusCode": 400, "correlationId": "e0hlizyl", } ) source = await async_get_media_source(hass) item = MediaSourceItem( hass, DOMAIN, f"{mock_config_entry.unique_id}|{collection}|unknown-id", None, ) root_media_source = await source.async_browse_media(item) assert root_media_source.title == collection assert len(root_media_source.children) == 0 async def test_media_view( hass: HomeAssistant, tmp_path: Path, mock_immich: Mock, mock_config_entry: MockConfigEntry, ) -> None: """Test ImmichMediaView returning albums.""" view = ImmichMediaView(hass) request = MockRequest(b"", DOMAIN) # immich not configured with pytest.raises(web.HTTPNotFound): await view.get(request, "", "") # setup immich assert await async_setup_component(hass, "media_source", {}) with patch("homeassistant.components.immich.PLATFORMS", []): await setup_integration(hass, mock_config_entry) # wrong url (without mime type) with pytest.raises(web.HTTPNotFound): await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail", ) # exception in async_view_asset() mock_immich.assets.async_view_asset.side_effect = ImmichError( { "message": "Not found or no asset.read access", "error": "Bad Request", "statusCode": 400, "correlationId": "e0hlizyl", } ) with pytest.raises(web.HTTPNotFound): await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail/image/jpeg", ) # exception in async_get_person_thumbnail() mock_immich.people.async_get_person_thumbnail.side_effect = ImmichError( { "message": "Not found or no asset.read access", "error": "Bad Request", "statusCode": 400, "correlationId": "e0hlizyl", } ) with pytest.raises(web.HTTPNotFound): await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/person/image/jpeg", ) # exception in async_play_video_stream() mock_immich.assets.async_play_video_stream.side_effect = ImmichError( { "message": "Not found or no asset.read access", "error": "Bad Request", "statusCode": 400, "correlationId": "e0hlizyl", } ) with pytest.raises(web.HTTPNotFound): await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/fullsize/video/mp4", ) # success mock_immich.assets.async_view_asset.side_effect = None mock_immich.assets.async_view_asset.return_value = b"xxxx" with patch.object(tempfile, "tempdir", tmp_path): result = await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail/image/jpeg", ) assert isinstance(result, web.Response) with patch.object(tempfile, "tempdir", tmp_path): result = await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/fullsize/image/jpeg", ) assert isinstance(result, web.Response) mock_immich.people.async_get_person_thumbnail.side_effect = None mock_immich.people.async_get_person_thumbnail.return_value = b"xxxx" with patch.object(tempfile, "tempdir", tmp_path): result = await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/person/image/jpeg", ) assert isinstance(result, web.Response) with patch.object(tempfile, "tempdir", tmp_path): result = await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/fullsize/image/jpeg", ) assert isinstance(result, web.Response) mock_immich.assets.async_play_video_stream.side_effect = None mock_immich.assets.async_play_video_stream.return_value = MockStreamReaderChunked( b"xxxx" ) with patch.object(tempfile, "tempdir", tmp_path): result = await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/fullsize/video/mp4", ) assert isinstance(result, web.StreamResponse) @pytest.mark.parametrize( ("identifier", "query", "expected_smart_search_call"), [ # search on config entry root (collections overview) ( "e7ef5713-9dab-4bd4-b899-715b0ca4379e", SearchMediaQuery(search_query="my search"), {"query": "my search", "page_size": 100, "max_pages": 1}, ), # search on config entry root (collections overview) with different media class filters ( "e7ef5713-9dab-4bd4-b899-715b0ca4379e", SearchMediaQuery( search_query="my search", media_filter_classes=[MediaClass.IMAGE] ), { "query": "my search", "page_size": 100, "max_pages": 1, "asset_type": AssetType.IMAGE, }, ), ( "e7ef5713-9dab-4bd4-b899-715b0ca4379e", SearchMediaQuery( search_query="my search", media_filter_classes=[MediaClass.VIDEO] ), { "query": "my search", "page_size": 100, "max_pages": 1, "asset_type": AssetType.VIDEO, }, ), ( "e7ef5713-9dab-4bd4-b899-715b0ca4379e", SearchMediaQuery( search_query="my search", media_filter_classes=[MediaClass.IMAGE, MediaClass.VIDEO], ), {"query": "my search", "page_size": 100, "max_pages": 1}, ), # search on album collection ( "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums", SearchMediaQuery(search_query="my search"), { "query": "my search", "page_size": 100, "max_pages": 1, "is_not_in_album": False, }, ), ( "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", SearchMediaQuery(search_query="my search"), { "query": "my search", "page_size": 100, "max_pages": 1, "is_not_in_album": False, "album_ids": ["721e1a4b-aa12-441e-8d3b-5ac7ab283bb6"], }, ), # search on people collection ( "e7ef5713-9dab-4bd4-b899-715b0ca4379e|people", SearchMediaQuery(search_query="my search"), {"query": "my search", "page_size": 100, "max_pages": 1}, ), ( "e7ef5713-9dab-4bd4-b899-715b0ca4379e|people|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", SearchMediaQuery(search_query="my search"), { "query": "my search", "page_size": 100, "max_pages": 1, "person_ids": ["721e1a4b-aa12-441e-8d3b-5ac7ab283bb6"], }, ), # search on tags collection ( "e7ef5713-9dab-4bd4-b899-715b0ca4379e|tags", SearchMediaQuery(search_query="my search"), {"query": "my search", "page_size": 100, "max_pages": 1}, ), ( "e7ef5713-9dab-4bd4-b899-715b0ca4379e|tags|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", SearchMediaQuery(search_query="my search"), { "query": "my search", "page_size": 100, "max_pages": 1, "tag_ids": ["721e1a4b-aa12-441e-8d3b-5ac7ab283bb6"], }, ), # search on favorites collection ( "e7ef5713-9dab-4bd4-b899-715b0ca4379e|favorites", SearchMediaQuery(search_query="my search"), { "query": "my search", "page_size": 100, "max_pages": 1, "is_favorite": True, }, ), ], ) async def test_search_media( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, identifier: str, query: SearchMediaQuery, expected_smart_search_call: dict[str, str | bool | list[str]], ) -> None: """Test search_media.""" assert await async_setup_component(hass, "media_source", {}) with patch("homeassistant.components.immich.PLATFORMS", []): await setup_integration(hass, mock_config_entry) source = await async_get_media_source(hass) item = MediaSourceItem(hass, "immich", identifier, None) result = await source.async_search_media(item, query) assert len(result.result) == 4 mock_immich.search.async_smart_search.assert_called_once_with( **expected_smart_search_call ) async def test_search_media_errors( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, ) -> None: """Test errors in search_media.""" assert await async_setup_component(hass, "media_source", {}) with patch("homeassistant.components.immich.PLATFORMS", []): await setup_integration(hass, mock_config_entry) source = await async_get_media_source(hass) # test generic ImmichError mock_immich.search.async_smart_search.side_effect = ImmichError( { "message": "Not found or no album.read access", "error": "Bad Request", "statusCode": 400, "correlationId": "e0hlizyl", } ) result = await source.async_search_media( MediaSourceItem(hass, "immich", "e7ef5713-9dab-4bd4-b899-715b0ca4379e", None), SearchMediaQuery(search_query="my search"), ) assert len(result.result) == 0 # test ImmichForbiddenError mock_immich.search.async_smart_search.side_effect = ImmichForbiddenError( { "message": "Missing required permission: asset.read", "error": "Forbidden", "statusCode": 403, "correlationId": "e0hlizyl", } ) with pytest.raises(BrowseError, match="Missing API permission"): await source.async_search_media( MediaSourceItem( hass, "immich", "e7ef5713-9dab-4bd4-b899-715b0ca4379e", None ), SearchMediaQuery(search_query="my search"), )