Add browse media support to squeezebox integration (#40642)
* Add browse media support to squeezebox integration * Move browse media logic to browse_media.py * Fix missing command when loading single url * Update .coveragerc * Handle empty library gracefully * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Implement suggestions from code review * Additional suggestion from code review * Use MEDIA_CLASS_GENRE Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
b3a97c7b42
commit
fcdb54d878
6 changed files with 228 additions and 9 deletions
|
@ -822,6 +822,7 @@ omit =
|
|||
homeassistant/components/spotify/__init__.py
|
||||
homeassistant/components/spotify/media_player.py
|
||||
homeassistant/components/squeezebox/__init__.py
|
||||
homeassistant/components/squeezebox/browse_media.py
|
||||
homeassistant/components/squeezebox/media_player.py
|
||||
homeassistant/components/starline/*
|
||||
homeassistant/components/starlingbank/sensor.py
|
||||
|
|
171
homeassistant/components/squeezebox/browse_media.py
Normal file
171
homeassistant/components/squeezebox/browse_media.py
Normal file
|
@ -0,0 +1,171 @@
|
|||
"""Support for media browsing."""
|
||||
from homeassistant.components.media_player import BrowseError, BrowseMedia
|
||||
from homeassistant.components.media_player.const import (
|
||||
MEDIA_CLASS_ALBUM,
|
||||
MEDIA_CLASS_ARTIST,
|
||||
MEDIA_CLASS_DIRECTORY,
|
||||
MEDIA_CLASS_GENRE,
|
||||
MEDIA_CLASS_PLAYLIST,
|
||||
MEDIA_CLASS_TRACK,
|
||||
MEDIA_TYPE_ALBUM,
|
||||
MEDIA_TYPE_ARTIST,
|
||||
MEDIA_TYPE_GENRE,
|
||||
MEDIA_TYPE_PLAYLIST,
|
||||
MEDIA_TYPE_TRACK,
|
||||
)
|
||||
|
||||
LIBRARY = ["Artists", "Albums", "Tracks", "Playlists", "Genres"]
|
||||
|
||||
MEDIA_TYPE_TO_SQUEEZEBOX = {
|
||||
"Artists": "artists",
|
||||
"Albums": "albums",
|
||||
"Tracks": "titles",
|
||||
"Playlists": "playlists",
|
||||
"Genres": "genres",
|
||||
MEDIA_TYPE_ALBUM: "album",
|
||||
MEDIA_TYPE_ARTIST: "artist",
|
||||
MEDIA_TYPE_TRACK: "title",
|
||||
MEDIA_TYPE_PLAYLIST: "playlist",
|
||||
MEDIA_TYPE_GENRE: "genre",
|
||||
}
|
||||
|
||||
SQUEEZEBOX_ID_BY_TYPE = {
|
||||
MEDIA_TYPE_ALBUM: "album_id",
|
||||
MEDIA_TYPE_ARTIST: "artist_id",
|
||||
MEDIA_TYPE_TRACK: "track_id",
|
||||
MEDIA_TYPE_PLAYLIST: "playlist_id",
|
||||
MEDIA_TYPE_GENRE: "genre_id",
|
||||
}
|
||||
|
||||
CONTENT_TYPE_MEDIA_CLASS = {
|
||||
"Artists": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_ARTIST},
|
||||
"Albums": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_ALBUM},
|
||||
"Tracks": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_TRACK},
|
||||
"Playlists": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_PLAYLIST},
|
||||
"Genres": {"item": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_GENRE},
|
||||
MEDIA_TYPE_ALBUM: {"item": MEDIA_CLASS_ALBUM, "children": MEDIA_CLASS_TRACK},
|
||||
MEDIA_TYPE_ARTIST: {"item": MEDIA_CLASS_ARTIST, "children": MEDIA_CLASS_ALBUM},
|
||||
MEDIA_TYPE_TRACK: {"item": MEDIA_CLASS_TRACK, "children": None},
|
||||
MEDIA_TYPE_GENRE: {"item": MEDIA_CLASS_GENRE, "children": MEDIA_CLASS_ARTIST},
|
||||
MEDIA_TYPE_PLAYLIST: {
|
||||
"item": MEDIA_CLASS_PLAYLIST,
|
||||
"children": MEDIA_CLASS_TRACK,
|
||||
},
|
||||
}
|
||||
|
||||
CONTENT_TYPE_TO_CHILD_TYPE = {
|
||||
MEDIA_TYPE_ALBUM: MEDIA_TYPE_TRACK,
|
||||
MEDIA_TYPE_PLAYLIST: MEDIA_TYPE_PLAYLIST,
|
||||
MEDIA_TYPE_ARTIST: MEDIA_TYPE_ALBUM,
|
||||
MEDIA_TYPE_GENRE: MEDIA_TYPE_ARTIST,
|
||||
"Artists": MEDIA_TYPE_ARTIST,
|
||||
"Albums": MEDIA_TYPE_ALBUM,
|
||||
"Tracks": MEDIA_TYPE_TRACK,
|
||||
"Playlists": MEDIA_TYPE_PLAYLIST,
|
||||
"Genres": MEDIA_TYPE_GENRE,
|
||||
}
|
||||
|
||||
BROWSE_LIMIT = 500
|
||||
|
||||
|
||||
async def build_item_response(player, payload):
|
||||
"""Create response payload for search described by payload."""
|
||||
search_id = payload["search_id"]
|
||||
search_type = payload["search_type"]
|
||||
|
||||
media_class = CONTENT_TYPE_MEDIA_CLASS[search_type]
|
||||
|
||||
if search_id and search_id != search_type:
|
||||
browse_id = (SQUEEZEBOX_ID_BY_TYPE[search_type], search_id)
|
||||
else:
|
||||
browse_id = None
|
||||
|
||||
result = await player.async_browse(
|
||||
MEDIA_TYPE_TO_SQUEEZEBOX[search_type],
|
||||
limit=BROWSE_LIMIT,
|
||||
browse_id=browse_id,
|
||||
)
|
||||
|
||||
children = None
|
||||
|
||||
if result is not None and result.get("items"):
|
||||
item_type = CONTENT_TYPE_TO_CHILD_TYPE[search_type]
|
||||
child_media_class = CONTENT_TYPE_MEDIA_CLASS[item_type]
|
||||
|
||||
children = []
|
||||
for item in result["items"]:
|
||||
children.append(
|
||||
BrowseMedia(
|
||||
title=item["title"],
|
||||
media_class=child_media_class["item"],
|
||||
media_content_id=str(item["id"]),
|
||||
media_content_type=item_type,
|
||||
can_play=True,
|
||||
can_expand=child_media_class["children"] is not None,
|
||||
thumbnail=item.get("image_url"),
|
||||
)
|
||||
)
|
||||
|
||||
if children is None:
|
||||
raise BrowseError(f"Media not found: {search_type} / {search_id}")
|
||||
|
||||
return BrowseMedia(
|
||||
title=result.get("title"),
|
||||
media_class=media_class["item"],
|
||||
children_media_class=media_class["children"],
|
||||
media_content_id=search_id,
|
||||
media_content_type=search_type,
|
||||
can_play=True,
|
||||
children=children,
|
||||
can_expand=True,
|
||||
)
|
||||
|
||||
|
||||
async def library_payload(player):
|
||||
"""Create response payload to describe contents of library."""
|
||||
|
||||
library_info = {
|
||||
"title": "Music Library",
|
||||
"media_class": MEDIA_CLASS_DIRECTORY,
|
||||
"media_content_id": "library",
|
||||
"media_content_type": "library",
|
||||
"can_play": False,
|
||||
"can_expand": True,
|
||||
"children": [],
|
||||
}
|
||||
|
||||
for item in LIBRARY:
|
||||
media_class = CONTENT_TYPE_MEDIA_CLASS[item]
|
||||
result = await player.async_browse(
|
||||
MEDIA_TYPE_TO_SQUEEZEBOX[item],
|
||||
limit=1,
|
||||
)
|
||||
if result is not None and result.get("items") is not None:
|
||||
library_info["children"].append(
|
||||
BrowseMedia(
|
||||
title=item,
|
||||
media_class=media_class["children"],
|
||||
media_content_id=item,
|
||||
media_content_type=item,
|
||||
can_play=True,
|
||||
can_expand=True,
|
||||
)
|
||||
)
|
||||
|
||||
response = BrowseMedia(**library_info)
|
||||
return response
|
||||
|
||||
|
||||
async def generate_playlist(player, payload):
|
||||
"""Generate playlist from browsing payload."""
|
||||
media_type = payload["search_type"]
|
||||
media_id = payload["search_id"]
|
||||
|
||||
if media_type not in SQUEEZEBOX_ID_BY_TYPE:
|
||||
return None
|
||||
|
||||
browse_id = (SQUEEZEBOX_ID_BY_TYPE[media_type], media_id)
|
||||
result = await player.async_browse(
|
||||
"titles", limit=BROWSE_LIMIT, browse_id=browse_id
|
||||
)
|
||||
return result.get("items")
|
|
@ -6,7 +6,7 @@
|
|||
"@rajlaud"
|
||||
],
|
||||
"requirements": [
|
||||
"pysqueezebox==0.3.1"
|
||||
"pysqueezebox==0.5.1"
|
||||
],
|
||||
"config_flow": true
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ from homeassistant.components.media_player.const import (
|
|||
ATTR_MEDIA_ENQUEUE,
|
||||
MEDIA_TYPE_MUSIC,
|
||||
MEDIA_TYPE_PLAYLIST,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
SUPPORT_CLEAR_PLAYLIST,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
|
@ -48,6 +49,7 @@ from homeassistant.helpers.dispatcher import (
|
|||
)
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .browse_media import build_item_response, generate_playlist, library_payload
|
||||
from .const import (
|
||||
DEFAULT_PORT,
|
||||
DISCOVERY_TASK,
|
||||
|
@ -71,7 +73,8 @@ _LOGGER = logging.getLogger(__name__)
|
|||
DISCOVERY_INTERVAL = 60
|
||||
|
||||
SUPPORT_SQUEEZEBOX = (
|
||||
SUPPORT_PAUSE
|
||||
SUPPORT_BROWSE_MEDIA
|
||||
| SUPPORT_PAUSE
|
||||
| SUPPORT_VOLUME_SET
|
||||
| SUPPORT_VOLUME_MUTE
|
||||
| SUPPORT_PREVIOUS_TRACK
|
||||
|
@ -476,15 +479,40 @@ class SqueezeBoxEntity(MediaPlayerEntity):
|
|||
If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the current playlist.
|
||||
"""
|
||||
cmd = "play"
|
||||
index = None
|
||||
|
||||
if kwargs.get(ATTR_MEDIA_ENQUEUE):
|
||||
cmd = "add"
|
||||
|
||||
if media_type == MEDIA_TYPE_PLAYLIST:
|
||||
content = json.loads(media_id)
|
||||
await self._player.async_load_playlist(content["urls"], cmd)
|
||||
await self._player.async_index(content["index"])
|
||||
else:
|
||||
if media_type == MEDIA_TYPE_MUSIC:
|
||||
await self._player.async_load_url(media_id, cmd)
|
||||
return
|
||||
|
||||
if media_type == MEDIA_TYPE_PLAYLIST:
|
||||
try:
|
||||
# a saved playlist by number
|
||||
payload = {
|
||||
"search_id": int(media_id),
|
||||
"search_type": MEDIA_TYPE_PLAYLIST,
|
||||
}
|
||||
playlist = await generate_playlist(self._player, payload)
|
||||
except ValueError:
|
||||
# a list of urls
|
||||
content = json.loads(media_id)
|
||||
playlist = content["urls"]
|
||||
index = content["index"]
|
||||
else:
|
||||
payload = {
|
||||
"search_id": media_id,
|
||||
"search_type": media_type,
|
||||
}
|
||||
playlist = await generate_playlist(self._player, payload)
|
||||
|
||||
_LOGGER.debug("Generated playlist: %s", playlist)
|
||||
|
||||
await self._player.async_load_playlist(playlist, cmd)
|
||||
if index is not None:
|
||||
await self._player.async_index(index)
|
||||
|
||||
async def async_set_shuffle(self, shuffle):
|
||||
"""Enable/disable shuffle mode."""
|
||||
|
@ -541,3 +569,22 @@ class SqueezeBoxEntity(MediaPlayerEntity):
|
|||
async def async_unsync(self):
|
||||
"""Unsync this Squeezebox player."""
|
||||
await self._player.async_unsync()
|
||||
|
||||
async def async_browse_media(self, media_content_type=None, media_content_id=None):
|
||||
"""Implement the websocket media browsing helper."""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Reached async_browse_media with content_type %s and content_id %s",
|
||||
media_content_type,
|
||||
media_content_id,
|
||||
)
|
||||
|
||||
if media_content_type in [None, "library"]:
|
||||
return await library_payload(self._player)
|
||||
|
||||
payload = {
|
||||
"search_type": media_content_type,
|
||||
"search_id": media_content_id,
|
||||
}
|
||||
|
||||
return await build_item_response(self._player, payload)
|
||||
|
|
|
@ -1691,7 +1691,7 @@ pysonos==0.0.35
|
|||
pyspcwebgw==0.4.0
|
||||
|
||||
# homeassistant.components.squeezebox
|
||||
pysqueezebox==0.3.1
|
||||
pysqueezebox==0.5.1
|
||||
|
||||
# homeassistant.components.stiebel_eltron
|
||||
pystiebeleltron==0.0.1.dev2
|
||||
|
|
|
@ -829,7 +829,7 @@ pysonos==0.0.35
|
|||
pyspcwebgw==0.4.0
|
||||
|
||||
# homeassistant.components.squeezebox
|
||||
pysqueezebox==0.3.1
|
||||
pysqueezebox==0.5.1
|
||||
|
||||
# homeassistant.components.syncthru
|
||||
pysyncthru==0.7.0
|
||||
|
|
Loading…
Add table
Reference in a new issue