mirror of
https://github.com/home-assistant/core.git
synced 2025-06-01 03:37:08 +00:00
Allow browsing favorites in Sonos media browser (#64082)
* Allow browsing favorites in Sonos media browser * Group favorites by type, add thumbnails * Update homeassistant/components/sonos/media_player.py * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Keep favorite groups ordering consistent * Skip root folder if only one child available Co-authored-by: Jason Lawrence <jjlawren@users.noreply.github.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
3265a09083
commit
2f18058fe7
@ -43,6 +43,7 @@ SONOS_GENRE = "genres"
|
||||
SONOS_ALBUM_ARTIST = "album_artists"
|
||||
SONOS_TRACKS = "tracks"
|
||||
SONOS_COMPOSER = "composers"
|
||||
SONOS_RADIO = "radio"
|
||||
|
||||
SONOS_STATE_PLAYING = "PLAYING"
|
||||
SONOS_STATE_TRANSITIONING = "TRANSITIONING"
|
||||
@ -76,6 +77,7 @@ SONOS_TO_MEDIA_CLASSES = {
|
||||
"object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST,
|
||||
"object.container.playlistContainer": MEDIA_CLASS_PLAYLIST,
|
||||
"object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK,
|
||||
"object.item.audioItem.audioBroadcast": MEDIA_CLASS_GENRE,
|
||||
}
|
||||
|
||||
SONOS_TO_MEDIA_TYPES = {
|
||||
@ -120,6 +122,7 @@ SONOS_TYPES_MAPPING = {
|
||||
"object.container.playlistContainer.sameArtist": SONOS_ARTIST,
|
||||
"object.container.playlistContainer": SONOS_PLAYLISTS,
|
||||
"object.item.audioItem.musicTrack": SONOS_TRACKS,
|
||||
"object.item.audioItem.audioBroadcast": SONOS_RADIO,
|
||||
}
|
||||
|
||||
LIBRARY_TITLES_MAPPING = {
|
||||
|
@ -33,6 +33,10 @@ class SonosFavorites(SonosHouseholdCoordinator):
|
||||
favorites = self._favorites.copy()
|
||||
return iter(favorites)
|
||||
|
||||
def lookup_by_item_id(self, item_id: str) -> DidlFavorite | None:
|
||||
"""Return the favorite object with the provided item_id."""
|
||||
return next((fav for fav in self._favorites if fav.item_id == item_id), None)
|
||||
|
||||
async def async_update_entities(
|
||||
self, soco: SoCo, update_id: int | None = None
|
||||
) -> None:
|
||||
|
@ -120,19 +120,60 @@ def item_payload(item, get_thumbnail_url=None):
|
||||
)
|
||||
|
||||
|
||||
def root_payload(media_library, favorites, get_thumbnail_url):
|
||||
"""Return root payload for Sonos."""
|
||||
has_local_library = bool(
|
||||
media_library.browse_by_idstring(
|
||||
"tracks",
|
||||
"",
|
||||
max_items=1,
|
||||
)
|
||||
)
|
||||
|
||||
if not (favorites or has_local_library):
|
||||
raise BrowseError("No media available")
|
||||
|
||||
if not has_local_library:
|
||||
return favorites_payload(favorites)
|
||||
if not favorites:
|
||||
return library_payload(media_library, get_thumbnail_url)
|
||||
|
||||
children = [
|
||||
BrowseMedia(
|
||||
title="Favorites",
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
media_content_id="",
|
||||
media_content_type="favorites",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
),
|
||||
BrowseMedia(
|
||||
title="Music Library",
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
media_content_id="",
|
||||
media_content_type="library",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
),
|
||||
]
|
||||
|
||||
return BrowseMedia(
|
||||
title="Sonos",
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
media_content_id="",
|
||||
media_content_type="root",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=children,
|
||||
)
|
||||
|
||||
|
||||
def library_payload(media_library, get_thumbnail_url=None):
|
||||
"""
|
||||
Create response payload to describe contents of a specific library.
|
||||
|
||||
Used by async_browse_media.
|
||||
"""
|
||||
if not media_library.browse_by_idstring(
|
||||
"tracks",
|
||||
"",
|
||||
max_items=1,
|
||||
):
|
||||
raise BrowseError("Local library not found")
|
||||
|
||||
children = []
|
||||
for item in media_library.browse():
|
||||
with suppress(UnknownMediaType):
|
||||
@ -149,6 +190,73 @@ def library_payload(media_library, get_thumbnail_url=None):
|
||||
)
|
||||
|
||||
|
||||
def favorites_payload(favorites):
|
||||
"""
|
||||
Create response payload to describe contents of a specific library.
|
||||
|
||||
Used by async_browse_media.
|
||||
"""
|
||||
children = []
|
||||
|
||||
group_types = {fav.reference.item_class for fav in favorites}
|
||||
for group_type in sorted(group_types):
|
||||
media_content_type = SONOS_TYPES_MAPPING[group_type]
|
||||
children.append(
|
||||
BrowseMedia(
|
||||
title=media_content_type.title(),
|
||||
media_class=SONOS_TO_MEDIA_CLASSES[group_type],
|
||||
media_content_id=group_type,
|
||||
media_content_type="favorites_folder",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
)
|
||||
)
|
||||
|
||||
return BrowseMedia(
|
||||
title="Favorites",
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
media_content_id="",
|
||||
media_content_type="favorites",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=children,
|
||||
)
|
||||
|
||||
|
||||
def favorites_folder_payload(favorites, media_content_id):
|
||||
"""Create response payload to describe all items of a type of favorite.
|
||||
|
||||
Used by async_browse_media.
|
||||
"""
|
||||
children = []
|
||||
content_type = SONOS_TYPES_MAPPING[media_content_id]
|
||||
|
||||
for favorite in favorites:
|
||||
if favorite.reference.item_class != media_content_id:
|
||||
continue
|
||||
children.append(
|
||||
BrowseMedia(
|
||||
title=favorite.title,
|
||||
media_class=SONOS_TO_MEDIA_CLASSES[favorite.reference.item_class],
|
||||
media_content_id=favorite.item_id,
|
||||
media_content_type="favorite_item_id",
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
thumbnail=getattr(favorite, "album_art_uri", None),
|
||||
)
|
||||
)
|
||||
|
||||
return BrowseMedia(
|
||||
title=content_type.title(),
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
media_content_id="",
|
||||
media_content_type="favorites",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=children,
|
||||
)
|
||||
|
||||
|
||||
def get_media_type(item):
|
||||
"""Extract media type of item."""
|
||||
if item.item_class == "object.item.audioItem.musicTrack":
|
||||
|
@ -13,6 +13,7 @@ from soco.core import (
|
||||
PLAY_MODE_BY_MEANING,
|
||||
PLAY_MODES,
|
||||
)
|
||||
from soco.data_structures import DidlFavorite
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerEntity
|
||||
@ -54,6 +55,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.network import is_internal_request
|
||||
|
||||
from . import media_browser
|
||||
from .const import (
|
||||
DATA_SONOS,
|
||||
DOMAIN as SONOS_DOMAIN,
|
||||
@ -67,7 +69,6 @@ from .const import (
|
||||
)
|
||||
from .entity import SonosEntity
|
||||
from .helpers import soco_error
|
||||
from .media_browser import build_item_response, get_media, library_payload
|
||||
from .speaker import SonosMedia, SonosSpeaker
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -419,22 +420,37 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
soco = self.coordinator.soco
|
||||
if source == SOURCE_LINEIN:
|
||||
soco.switch_to_line_in()
|
||||
elif source == SOURCE_TV:
|
||||
return
|
||||
|
||||
if source == SOURCE_TV:
|
||||
soco.switch_to_tv()
|
||||
return
|
||||
|
||||
self._play_favorite_by_name(source)
|
||||
|
||||
def _play_favorite_by_name(self, name: str) -> None:
|
||||
"""Play a favorite by name."""
|
||||
fav = [fav for fav in self.speaker.favorites if fav.title == name]
|
||||
|
||||
if len(fav) != 1:
|
||||
return
|
||||
|
||||
src = fav.pop()
|
||||
self._play_favorite(src)
|
||||
|
||||
def _play_favorite(self, favorite: DidlFavorite) -> None:
|
||||
"""Play a favorite."""
|
||||
uri = favorite.reference.get_uri()
|
||||
soco = self.coordinator.soco
|
||||
if soco.music_source_from_uri(uri) in [
|
||||
MUSIC_SRC_RADIO,
|
||||
MUSIC_SRC_LINE_IN,
|
||||
]:
|
||||
soco.play_uri(uri, title=favorite.title)
|
||||
else:
|
||||
fav = [fav for fav in self.speaker.favorites if fav.title == source]
|
||||
if len(fav) == 1:
|
||||
src = fav.pop()
|
||||
uri = src.reference.get_uri()
|
||||
if soco.music_source_from_uri(uri) in [
|
||||
MUSIC_SRC_RADIO,
|
||||
MUSIC_SRC_LINE_IN,
|
||||
]:
|
||||
soco.play_uri(uri, title=source)
|
||||
else:
|
||||
soco.clear_queue()
|
||||
soco.add_to_queue(src.reference)
|
||||
soco.play_from_queue(0)
|
||||
soco.clear_queue()
|
||||
soco.add_to_queue(favorite.reference)
|
||||
soco.play_from_queue(0)
|
||||
|
||||
@property # type: ignore[misc]
|
||||
def source_list(self) -> list[str]:
|
||||
@ -501,6 +517,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
|
||||
If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
|
||||
"""
|
||||
if media_type == "favorite_item_id":
|
||||
favorite = self.speaker.favorites.lookup_by_item_id(media_id)
|
||||
if favorite is None:
|
||||
raise ValueError(f"Missing favorite for media_id: {media_id}")
|
||||
self._play_favorite(favorite)
|
||||
return
|
||||
|
||||
soco = self.coordinator.soco
|
||||
if media_id and media_id.startswith(PLEX_URI_SCHEME):
|
||||
media_id = media_id[len(PLEX_URI_SCHEME) :]
|
||||
@ -522,7 +545,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
soco.play_uri(media_id)
|
||||
elif media_type == MEDIA_TYPE_PLAYLIST:
|
||||
if media_id.startswith("S:"):
|
||||
item = get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call]
|
||||
item = media_browser.get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call]
|
||||
soco.play_uri(item.get_uri())
|
||||
return
|
||||
try:
|
||||
@ -535,7 +558,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
soco.add_to_queue(playlist)
|
||||
soco.play_from_queue(0)
|
||||
elif media_type in PLAYABLE_MEDIA_TYPES:
|
||||
item = get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call]
|
||||
item = media_browser.get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call]
|
||||
|
||||
if not item:
|
||||
_LOGGER.error('Could not find "%s" in the library', media_id)
|
||||
@ -616,7 +639,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
and media_content_id
|
||||
):
|
||||
item = await self.hass.async_add_executor_job(
|
||||
get_media,
|
||||
media_browser.get_media,
|
||||
self.media.library,
|
||||
media_content_id,
|
||||
MEDIA_TYPES_TO_SONOS[media_content_type],
|
||||
@ -639,7 +662,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
media_image_id: str | None = None,
|
||||
) -> str | None:
|
||||
if is_internal:
|
||||
item = get_media( # type: ignore[no-untyped-call]
|
||||
item = media_browser.get_media( # type: ignore[no-untyped-call]
|
||||
self.media.library,
|
||||
media_content_id,
|
||||
media_content_type,
|
||||
@ -652,9 +675,30 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
media_image_id,
|
||||
)
|
||||
|
||||
if media_content_type in [None, "library"]:
|
||||
if media_content_type in [None, "root"]:
|
||||
return await self.hass.async_add_executor_job(
|
||||
library_payload, self.media.library, _get_thumbnail_url
|
||||
media_browser.root_payload,
|
||||
self.media.library,
|
||||
self.speaker.favorites,
|
||||
_get_thumbnail_url,
|
||||
)
|
||||
|
||||
if media_content_type == "library":
|
||||
return await self.hass.async_add_executor_job(
|
||||
media_browser.library_payload, self.media.library, _get_thumbnail_url
|
||||
)
|
||||
|
||||
if media_content_type == "favorites":
|
||||
return await self.hass.async_add_executor_job(
|
||||
media_browser.favorites_payload,
|
||||
self.speaker.favorites,
|
||||
)
|
||||
|
||||
if media_content_type == "favorites_folder":
|
||||
return await self.hass.async_add_executor_job(
|
||||
media_browser.favorites_folder_payload,
|
||||
self.speaker.favorites,
|
||||
media_content_id,
|
||||
)
|
||||
|
||||
payload = {
|
||||
@ -662,7 +706,10 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
"idstring": media_content_id,
|
||||
}
|
||||
response = await self.hass.async_add_executor_job(
|
||||
build_item_response, self.media.library, payload, _get_thumbnail_url
|
||||
media_browser.build_item_response,
|
||||
self.media.library,
|
||||
payload,
|
||||
_get_thumbnail_url,
|
||||
)
|
||||
if response is None:
|
||||
raise BrowseError(
|
||||
|
Loading…
x
Reference in New Issue
Block a user