"""Tests for the Sonos Media Browser.""" from functools import partial from unittest.mock import MagicMock, Mock import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, BrowseMedia, MediaClass, MediaType, ) from homeassistant.components.sonos.const import MEDIA_TYPE_DIRECTORY, SONOS_TRACKS from homeassistant.components.sonos.media_browser import ( build_item_response, get_media, get_thumbnail_url_full, ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from .conftest import MockMusicServiceItem, SoCoMockFactory from tests.typing import WebSocketGenerator def mock_browse_by_idstring( search_type: str, idstring: str, start=0, max_items=100, full_album_art_uri=False ) -> list[MockMusicServiceItem]: """Mock the call to browse_by_id_string.""" if search_type == "albums" and idstring in ( "A:ALBUM/Abbey%20Road", "A:ALBUM/Abbey Road", ): return [ MockMusicServiceItem( "Come Together", "S://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3", "A:ALBUM/Abbey%20Road", "object.item.audioItem.musicTrack", ), MockMusicServiceItem( "Something", "S://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3", "A:ALBUM/Abbey%20Road", "object.item.audioItem.musicTrack", ), ] return None async def test_build_item_response( hass: HomeAssistant, soco_factory: SoCoMockFactory, async_autosetup_sonos, soco, discover, ) -> None: """Test building a browse item response.""" soco_mock = soco_factory.mock_list.get("192.168.42.2") soco_mock.music_library.browse_by_idstring = mock_browse_by_idstring browse_item: BrowseMedia = build_item_response( soco_mock.music_library, {"search_type": MediaType.ALBUM, "idstring": "A:ALBUM/Abbey%20Road"}, partial( get_thumbnail_url_full, soco_mock.music_library, True, None, ), ) assert browse_item.title == "Abbey Road" assert browse_item.media_class == MediaClass.ALBUM assert browse_item.media_content_id == "A:ALBUM/Abbey%20Road" assert len(browse_item.children) == 2 assert browse_item.children[0].media_class == MediaClass.TRACK assert browse_item.children[0].title == "Come Together" assert ( browse_item.children[0].media_content_id == "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3" ) assert browse_item.children[1].media_class == MediaClass.TRACK assert browse_item.children[1].title == "Something" assert ( browse_item.children[1].media_content_id == "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3" ) def test_get_media_multisegment_album_id_uses_album_segment() -> None: """Test `A:ALBUM//` uses album name as lookup search term.""" music_library = MagicMock() music_library.get_music_library_information.return_value = [] result = get_media( music_library, "A:ALBUM/Abbey%20Road/The%20Beatles", "album", ) assert result is None assert music_library.get_music_library_information.call_count == 1 assert music_library.get_music_library_information.call_args.args == ("albums",) assert music_library.get_music_library_information.call_args.kwargs == { "search_term": "Abbey Road", "full_album_art_uri": True, } def test_get_media_multisegment_album_id_prefers_exact_item_id_match() -> None: """Test multi-match disambiguation prefers exact `item_id`.""" music_library = MagicMock() exact_item = MockMusicServiceItem( "Abbey Road (Remaster)", "A:ALBUM/Abbey%20Road/The%20Beatles", "A:ALBUM", "object.container.album.musicAlbum", ) music_library.get_music_library_information.return_value = [ MockMusicServiceItem( "Abbey Road", "A:ALBUM/Abbey%20Road/Someone%20Else", "A:ALBUM", "object.container.album.musicAlbum", ), exact_item, ] result = get_media( music_library, "A:ALBUM/Abbey%20Road/The%20Beatles", "album", ) assert result is exact_item def test_get_media_multisegment_album_id_falls_back_to_exact_title_match() -> None: """Test multi-match disambiguation falls back to exact title match.""" music_library = MagicMock() title_match_item = MockMusicServiceItem( "Abbey Road", "A:ALBUM/Abbey%20Road/The%20Beatles%20(Remaster)", "A:ALBUM", "object.container.album.musicAlbum", ) music_library.get_music_library_information.return_value = [ MockMusicServiceItem( "Abbey Road (Live)", "A:ALBUM/Abbey%20Road/The%20Beatles%20(Live)", "A:ALBUM", "object.container.album.musicAlbum", ), title_match_item, ] result = get_media( music_library, "A:ALBUM/Abbey%20Road/The%20Beatles", "album", ) assert result is title_match_item async def test_browse_media_root( hass: HomeAssistant, soco_factory: SoCoMockFactory, async_autosetup_sonos, soco, discover, hass_ws_client: WebSocketGenerator, snapshot: SnapshotAssertion, ) -> None: """Test the async_browse_media method.""" client = await hass_ws_client() await client.send_json( { "id": 1, "type": "media_player/browse_media", "entity_id": "media_player.zone_a", } ) response = await client.receive_json() assert response["success"] assert response["result"]["children"] == snapshot async def test_browse_media_library( hass: HomeAssistant, soco_factory: SoCoMockFactory, async_autosetup_sonos, soco, discover, hass_ws_client: WebSocketGenerator, snapshot: SnapshotAssertion, ) -> None: """Test the async_browse_media method.""" client = await hass_ws_client() await client.send_json( { "id": 1, "type": "media_player/browse_media", "entity_id": "media_player.zone_a", "media_content_id": "", "media_content_type": "library", } ) response = await client.receive_json() assert response["success"] assert response["result"]["children"] == snapshot async def test_browse_media_library_albums( hass: HomeAssistant, soco_factory: SoCoMockFactory, async_autosetup_sonos, soco, discover, hass_ws_client: WebSocketGenerator, snapshot: SnapshotAssertion, ) -> None: """Test the async_browse_media method.""" soco_mock = soco_factory.mock_list.get("192.168.42.2") client = await hass_ws_client() await client.send_json( { "id": 1, "type": "media_player/browse_media", "entity_id": "media_player.zone_a", "media_content_id": "A:ALBUM", "media_content_type": "album", } ) response = await client.receive_json() assert response["success"] assert response["result"]["children"] == snapshot assert soco_mock.music_library.browse_by_idstring.call_count == 1 @pytest.mark.parametrize( ("media_content_id", "media_content_type"), [ ( "", "favorites", ), ( "object.item.audioItem.audioBook", "favorites_folder", ), ( "object.container.album.musicAlbum", "favorites_folder", ), ( "object.container.podcast", "favorites_folder", ), ], ) async def test_browse_media_favorites( async_autosetup_sonos, hass_ws_client: WebSocketGenerator, snapshot: SnapshotAssertion, media_content_id, media_content_type, ) -> None: """Test the async_browse_media method.""" client = await hass_ws_client() await client.send_json( { "id": 1, "type": "media_player/browse_media", "entity_id": "media_player.zone_a", "media_content_id": media_content_id, "media_content_type": media_content_type, } ) response = await client.receive_json() assert response["success"] assert response["result"] == snapshot @pytest.mark.parametrize( "media_content_id", [ ("S:"), ("S://192.168.1.1/music"), ], ) async def test_browse_media_library_folders( hass: HomeAssistant, soco_factory: SoCoMockFactory, async_autosetup_sonos, media_content_id: str, hass_ws_client: WebSocketGenerator, snapshot: SnapshotAssertion, ) -> None: """Test the async_browse_media method.""" soco_mock = soco_factory.mock_list.get("192.168.42.2") client = await hass_ws_client() await client.send_json( { "id": 1, "type": "media_player/browse_media", ATTR_ENTITY_ID: "media_player.zone_a", ATTR_MEDIA_CONTENT_ID: media_content_id, ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_DIRECTORY, } ) response = await client.receive_json() assert response["success"] assert response["result"] == snapshot assert soco_mock.music_library.browse_by_idstring.call_count == 1 async def test_search_media( hass: HomeAssistant, soco_factory: SoCoMockFactory, async_autosetup_sonos, soco, discover, hass_ws_client: WebSocketGenerator, snapshot: SnapshotAssertion, ) -> None: """Test the async_search_media method returns tracks matching the query.""" soco_mock = soco_factory.mock_list.get("192.168.42.2") mock_items = [ MockMusicServiceItem( "Come Together", "S://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3", "A:ALBUM/Abbey%20Road", "object.item.audioItem.musicTrack", album_art_uri="http://example.com/abbey_road.jpg", ), MockMusicServiceItem( "Something", "S://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3", "A:ALBUM/Abbey%20Road", "object.item.audioItem.musicTrack", album_art_uri="http://example.com/abbey_road.jpg", ), ] soco_mock.music_library.get_music_library_information = Mock( return_value=mock_items ) client = await hass_ws_client() await client.send_json( { "id": 1, "type": "media_player/search_media", "entity_id": "media_player.zone_a", "search_query": "Come Together", } ) response = await client.receive_json() assert response["success"] assert response["result"] == snapshot assert soco_mock.music_library.get_music_library_information.call_args.args == ( SONOS_TRACKS, ) assert soco_mock.music_library.get_music_library_information.call_args.kwargs == { "search_term": "Come Together", "full_album_art_uri": True, "complete_result": True, } async def test_search_media_invalid_media_content_type( hass: HomeAssistant, async_autosetup_sonos, hass_ws_client: WebSocketGenerator, ) -> None: """Test that async_search_media raises on an unsupported media_content_type.""" client = await hass_ws_client() await client.send_json( { "id": 1, "type": "media_player/search_media", "entity_id": "media_player.zone_a", "media_content_type": "movie", "media_content_id": "some_id", "search_query": "test", } ) response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "home_assistant_error" assert response["error"]["translation_key"] == "invalid_media_content_type" assert response["error"]["translation_placeholders"] == { "media_content_type": "movie" }