diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py index 1ae5804ea66a8..81cdfdfb3cfb0 100644 --- a/homeassistant/components/spotify/browse_media.py +++ b/homeassistant/components/spotify/browse_media.py @@ -14,6 +14,7 @@ SpotifyClient, Track, ) +from spotifyaio.models import ItemType, SimplifiedEpisode import yarl from homeassistant.components.media_player import ( @@ -90,6 +91,16 @@ def _get_track_item_payload( } +def _get_episode_item_payload(episode: SimplifiedEpisode) -> ItemPayload: + return { + "id": episode.episode_id, + "name": episode.name, + "type": MediaType.EPISODE, + "uri": episode.uri, + "thumbnail": fetch_image_url(episode.images), + } + + class BrowsableMedia(StrEnum): """Enum of browsable media.""" @@ -345,10 +356,15 @@ async def build_item_response( # noqa: C901 if playlist := await spotify.get_playlist(media_content_id): title = playlist.name image = playlist.images[0].url if playlist.images else None - items = [ - _get_track_item_payload(playlist_track.track) - for playlist_track in playlist.tracks.items - ] + for playlist_item in playlist.tracks.items: + if playlist_item.track.type is ItemType.TRACK: + if TYPE_CHECKING: + assert isinstance(playlist_item.track, Track) + items.append(_get_track_item_payload(playlist_item.track)) + elif playlist_item.track.type is ItemType.EPISODE: + if TYPE_CHECKING: + assert isinstance(playlist_item.track, SimplifiedEpisode) + items.append(_get_episode_item_payload(playlist_item.track)) elif media_content_type == MediaType.ALBUM: if album := await spotify.get_album(media_content_id): title = album.name @@ -370,16 +386,7 @@ async def build_item_response( # noqa: C901 ): title = show.name image = show.images[0].url if show.images else None - items = [ - { - "id": episode.episode_id, - "name": episode.name, - "type": MediaType.EPISODE, - "uri": episode.uri, - "thumbnail": fetch_image_url(episode.images), - } - for episode in show_episodes - ] + items = [_get_episode_item_payload(episode) for episode in show_episodes] try: media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type] diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index e7b24cb3e1d86..6c5b7382bbbda 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/spotify", "integration_type": "service", "iot_class": "cloud_polling", - "loggers": ["spotipy"], - "requirements": ["spotifyaio==0.8.8"], + "loggers": ["spotifyaio"], + "requirements": ["spotifyaio==0.8.10"], "zeroconf": ["_spotify-connect._tcp.local."] } diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 7687936fe4cd9..20a634efb4244 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -361,6 +361,8 @@ async def async_select_source(self, source: str) -> None: """Select playback device.""" for device in self.devices.data: if device.name == source: + if TYPE_CHECKING: + assert device.device_id is not None await self.coordinator.client.transfer_playback(device.device_id) return diff --git a/requirements_all.txt b/requirements_all.txt index fc13e72c128df..9a4c93ee96e01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2719,7 +2719,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spotify -spotifyaio==0.8.8 +spotifyaio==0.8.10 # homeassistant.components.sql sqlparse==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0923b49757552..b17bd38a84928 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2171,7 +2171,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spotify -spotifyaio==0.8.8 +spotifyaio==0.8.10 # homeassistant.components.sql sqlparse==0.5.0 diff --git a/tests/components/spotify/fixtures/playlist.json b/tests/components/spotify/fixtures/playlist.json index 36c28cc814b7d..5680ac9109c71 100644 --- a/tests/components/spotify/fixtures/playlist.json +++ b/tests/components/spotify/fixtures/playlist.json @@ -514,6 +514,472 @@ "uri": "spotify:track:2E2znCPaS8anQe21GLxcvJ", "is_local": false } + }, + { + "added_at": "2024-11-28T11:20:58Z", + "added_by": { + "external_urls": { + "spotify": "https://open.spotify.com/user/1112264649" + }, + "href": "https://api.spotify.com/v1/users/1112264649", + "id": "1112264649", + "type": "user", + "uri": "spotify:user:1112264649" + }, + "is_local": false, + "primary_color": null, + "track": { + "explicit": false, + "audio_preview_url": "https://podz-content.spotifycdn.com/audio/clips/06lRxUmh8UNVTByuyxLYqh/clip_132296_192296.mp3", + "description": "Patreon: https://www.patreon.com/safetythirdMerch: https://safetythird.shopYouTube: https://www.youtube.com/@safetythird/Advertising Inquiries: https://redcircle.com/brandsPrivacy & Opt-Out: https://redcircle.com/privacy", + "duration_ms": 3690161, + "episode": true, + "external_urls": { + "spotify": "https://open.spotify.com/episode/3o0RYoo5iOMKSmEbunsbvW" + }, + "href": "https://api.spotify.com/v1/episodes/3o0RYoo5iOMKSmEbunsbvW", + "html_description": "
Patreon: https://www.patreon.com/safetythird
Merch: https://safetythird.shop
YouTube: https://www.youtube.com/@safetythird/
Safety Third is a weekly show hosted by William Osman, NileRed, The Backyard Scientist, Allen Pan, and a couple other YouTube "Scientists". Sometimes we have guests, sometimes it's just us, but always: safety is our number three priority.
", + "id": "1Y9ExMgMxoBVrgrfU7u0nD", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab6765630000ba8ac7bedd27a4413b1abf926d8a", + "width": 640 + }, + { + "height": 300, + "url": "https://i.scdn.co/image/ab67656300005f1fc7bedd27a4413b1abf926d8a", + "width": 300 + }, + { + "height": 64, + "url": "https://i.scdn.co/image/ab6765630000f68dc7bedd27a4413b1abf926d8a", + "width": 64 + } + ], + "is_externally_hosted": false, + "languages": ["en-US"], + "media_type": "audio", + "name": "Safety Third", + "publisher": "Safety Third ", + "total_episodes": 120, + "type": "show", + "uri": "spotify:show:1Y9ExMgMxoBVrgrfU7u0nD" + }, + "track": false, + "type": "episode", + "uri": "spotify:episode:3o0RYoo5iOMKSmEbunsbvW" + }, + "video_thumbnail": { + "url": null + } } ] } diff --git a/tests/components/spotify/snapshots/test_diagnostics.ambr b/tests/components/spotify/snapshots/test_diagnostics.ambr index 40502562da36f..0ac375d18e31d 100644 --- a/tests/components/spotify/snapshots/test_diagnostics.ambr +++ b/tests/components/spotify/snapshots/test_diagnostics.ambr @@ -409,6 +409,69 @@ 'uri': 'spotify:track:2E2znCPaS8anQe21GLxcvJ', }), }), + dict({ + 'track': dict({ + 'description': 'Patreon: https://www.patreon.com/safetythirdMerch: https://safetythird.shopYouTube: https://www.youtube.com/@safetythird/Advertising Inquiries: https://redcircle.com/brandsPrivacy & Opt-Out: https://redcircle.com/privacy', + 'duration_ms': 3690161, + 'episode_id': '3o0RYoo5iOMKSmEbunsbvW', + 'explicit': False, + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/episode/3o0RYoo5iOMKSmEbunsbvW', + }), + 'href': 'https://api.spotify.com/v1/episodes/3o0RYoo5iOMKSmEbunsbvW', + 'images': list([ + dict({ + 'height': 640, + 'url': 'https://i.scdn.co/image/ab6765630000ba8ac7bedd27a4413b1abf926d8a', + 'width': 640, + }), + dict({ + 'height': 300, + 'url': 'https://i.scdn.co/image/ab67656300005f1fc7bedd27a4413b1abf926d8a', + 'width': 300, + }), + dict({ + 'height': 64, + 'url': 'https://i.scdn.co/image/ab6765630000f68dc7bedd27a4413b1abf926d8a', + 'width': 64, + }), + ]), + 'name': 'My Squirrel Has Brain Damage - Safety Third 119', + 'release_date': '2024-07-26', + 'release_date_precision': 'day', + 'show': dict({ + 'description': 'Safety Third is a weekly show hosted by William Osman, NileRed, The Backyard Scientist, Allen Pan, and a couple other YouTube "Scientists". Sometimes we have guests, sometimes it\'s just us, but always: safety is our number three priority.', + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/show/1Y9ExMgMxoBVrgrfU7u0nD', + }), + 'href': 'https://api.spotify.com/v1/shows/1Y9ExMgMxoBVrgrfU7u0nD', + 'images': list([ + dict({ + 'height': 640, + 'url': 'https://i.scdn.co/image/ab6765630000ba8ac7bedd27a4413b1abf926d8a', + 'width': 640, + }), + dict({ + 'height': 300, + 'url': 'https://i.scdn.co/image/ab67656300005f1fc7bedd27a4413b1abf926d8a', + 'width': 300, + }), + dict({ + 'height': 64, + 'url': 'https://i.scdn.co/image/ab6765630000f68dc7bedd27a4413b1abf926d8a', + 'width': 64, + }), + ]), + 'name': 'Safety Third', + 'publisher': 'Safety Third ', + 'show_id': '1Y9ExMgMxoBVrgrfU7u0nD', + 'total_episodes': 120, + 'uri': 'spotify:show:1Y9ExMgMxoBVrgrfU7u0nD', + }), + 'type': 'episode', + 'uri': 'spotify:episode:3o0RYoo5iOMKSmEbunsbvW', + }), + }), ]), }), 'uri': 'spotify:playlist:3cEYpjA9oz9GiPac4AsH4n', diff --git a/tests/components/spotify/snapshots/test_media_browser.ambr b/tests/components/spotify/snapshots/test_media_browser.ambr index 764dc7a10e159..6b21797722742 100644 --- a/tests/components/spotify/snapshots/test_media_browser.ambr +++ b/tests/components/spotify/snapshots/test_media_browser.ambr @@ -649,6 +649,16 @@ 'thumbnail': 'https://i.scdn.co/image/ab67616d0000b27304e57d181ff062f8339d6c71', 'title': 'You Are So Beautiful', }), + dict({ + 'can_expand': False, + 'can_play': True, + 'children_media_class': None, + 'media_class':