mirror of
https://github.com/natekspencer/hacs-oasis_mini.git
synced 2025-12-06 18:44:14 -05:00
Enhance media_player with browse/search capability
This commit is contained in:
@@ -3,9 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
BrowseError,
|
||||
BrowseMedia,
|
||||
MediaPlayerEnqueue,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityDescription,
|
||||
@@ -13,12 +16,27 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerState,
|
||||
MediaType,
|
||||
RepeatMode,
|
||||
SearchMedia,
|
||||
SearchMediaQuery,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import OasisDeviceConfigEntry, setup_platform_from_coordinator
|
||||
from .browse_media import (
|
||||
MEDIA_TYPE_OASIS_PLAYLIST,
|
||||
MEDIA_TYPE_OASIS_PLAYLISTS,
|
||||
MEDIA_TYPE_OASIS_ROOT,
|
||||
MEDIA_TYPE_OASIS_TRACK,
|
||||
MEDIA_TYPE_OASIS_TRACKS,
|
||||
async_search_media,
|
||||
build_playlist_item,
|
||||
build_playlists_root,
|
||||
build_root_response,
|
||||
build_track_item,
|
||||
build_tracks_root,
|
||||
)
|
||||
from .const import DOMAIN
|
||||
from .entity import OasisDeviceEntity
|
||||
from .helpers import get_track_id
|
||||
@@ -33,6 +51,9 @@ from .pyoasiscontrol.const import (
|
||||
STATUS_STOPPED,
|
||||
STATUS_UPDATING,
|
||||
)
|
||||
from .pyoasiscontrol.utils import get_track_ids_from_playlist
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -66,6 +87,7 @@ DESCRIPTOR = MediaPlayerEntityDescription(key="oasis_mini", name=None)
|
||||
class OasisDeviceMediaPlayerEntity(OasisDeviceEntity, MediaPlayerEntity):
|
||||
"""Oasis device media player entity."""
|
||||
|
||||
_attr_media_content_type = MediaType.IMAGE
|
||||
_attr_media_image_remotely_accessible = True
|
||||
_attr_supported_features = (
|
||||
MediaPlayerEntityFeature.PAUSE
|
||||
@@ -77,13 +99,10 @@ class OasisDeviceMediaPlayerEntity(OasisDeviceEntity, MediaPlayerEntity):
|
||||
| MediaPlayerEntityFeature.MEDIA_ENQUEUE
|
||||
| MediaPlayerEntityFeature.CLEAR_PLAYLIST
|
||||
| MediaPlayerEntityFeature.REPEAT_SET
|
||||
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||
| MediaPlayerEntityFeature.SEARCH_MEDIA
|
||||
)
|
||||
|
||||
@property
|
||||
def media_content_type(self) -> MediaType:
|
||||
"""Content type of current playing media."""
|
||||
return MediaType.IMAGE
|
||||
|
||||
@property
|
||||
def media_duration(self) -> int | None:
|
||||
"""Duration of current playing media in seconds."""
|
||||
@@ -244,21 +263,66 @@ class OasisDeviceMediaPlayerEntity(OasisDeviceEntity, MediaPlayerEntity):
|
||||
"""
|
||||
Play or enqueue one or more Oasis tracks on the device.
|
||||
|
||||
Validates the media type and parses one or more track identifiers from `media_id`, then updates the device playlist according to `enqueue`. Depending on the enqueue mode the method can replace the playlist, append tracks, move appended tracks to the next play position, and optionally start playback.
|
||||
Validates the media type and parses one or more track identifiers from
|
||||
`media_id`, then updates the device playlist according to `enqueue`. Depending
|
||||
on the enqueue mode the method can replace the playlist, append tracks, move
|
||||
appended tracks to the next play position, and optionally start playback.
|
||||
|
||||
Parameters:
|
||||
media_type (MediaType | str): The media type being requested.
|
||||
media_id (str): A comma-separated string of track identifiers.
|
||||
enqueue (MediaPlayerEnqueue | None): How to insert the tracks into the playlist; if omitted defaults to NEXT.
|
||||
enqueue (MediaPlayerEnqueue | None): How to insert the tracks into the playlist; if omitted defaults to PLAY.
|
||||
|
||||
Raises:
|
||||
ServiceValidationError: If the device is busy, if `media_type` is a playlist (playlists are unsupported), or if `media_id` does not contain any valid track identifiers.
|
||||
ServiceValidationError: If the device is busy or if `media_id` does not contain any valid media identifiers.
|
||||
"""
|
||||
self.abort_if_busy()
|
||||
if media_type == MediaType.PLAYLIST:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN, translation_key="playlists_unsupported"
|
||||
)
|
||||
|
||||
track_ids: list[int] = []
|
||||
|
||||
# Entire playlist from browse
|
||||
if media_type == MEDIA_TYPE_OASIS_PLAYLIST:
|
||||
try:
|
||||
playlist_id = int(media_id)
|
||||
except (TypeError, ValueError) as err:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_media",
|
||||
translation_placeholders={"media": f"playlist {media_id}"},
|
||||
) from err
|
||||
|
||||
playlists = await self.coordinator.cloud_client.async_get_playlists()
|
||||
playlist = next((p for p in playlists if p.get("id") == playlist_id), None)
|
||||
if not playlist:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_media",
|
||||
translation_placeholders={"media": f"playlist {playlist_id}"},
|
||||
)
|
||||
|
||||
track_ids = get_track_ids_from_playlist(playlist)
|
||||
|
||||
if not track_ids:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_media",
|
||||
translation_placeholders={
|
||||
"media": f"playlist {playlist_id} is empty"
|
||||
},
|
||||
)
|
||||
|
||||
elif media_type == MEDIA_TYPE_OASIS_TRACK:
|
||||
try:
|
||||
track_id = int(media_id)
|
||||
except (TypeError, ValueError) as err:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_media",
|
||||
translation_placeholders={"media": f"track {media_id}"},
|
||||
) from err
|
||||
|
||||
track_ids = [track_id]
|
||||
|
||||
else:
|
||||
track = list(filter(None, map(get_track_id, media_id.split(","))))
|
||||
if not track:
|
||||
@@ -269,29 +333,33 @@ class OasisDeviceMediaPlayerEntity(OasisDeviceEntity, MediaPlayerEntity):
|
||||
)
|
||||
|
||||
device = self.device
|
||||
enqueue = MediaPlayerEnqueue.NEXT if not enqueue else enqueue
|
||||
enqueue = MediaPlayerEnqueue.PLAY if not enqueue else enqueue
|
||||
|
||||
if enqueue == MediaPlayerEnqueue.ADD:
|
||||
await device.async_add_track_to_playlist(track_ids)
|
||||
return
|
||||
|
||||
if enqueue == MediaPlayerEnqueue.REPLACE:
|
||||
await device.async_set_playlist(track)
|
||||
else:
|
||||
await device.async_add_track_to_playlist(track)
|
||||
await device.async_stop()
|
||||
await device.async_set_playlist(track_ids)
|
||||
await device.async_play()
|
||||
return
|
||||
|
||||
if enqueue in (MediaPlayerEnqueue.NEXT, MediaPlayerEnqueue.PLAY):
|
||||
# Move track to next item in the playlist
|
||||
new_tracks = 1 if isinstance(track, int) else len(track)
|
||||
if (index := (len(device.playlist) - new_tracks)) != device.playlist_index:
|
||||
if index != (
|
||||
_next := min(
|
||||
device.playlist_index + 1, len(device.playlist) - new_tracks
|
||||
)
|
||||
):
|
||||
await device.async_move_track(index, _next)
|
||||
if enqueue == MediaPlayerEnqueue.PLAY:
|
||||
await device.async_change_track(_next)
|
||||
insert_at = (device.playlist_index or 0) + 1
|
||||
original_len = len(device.playlist)
|
||||
await device.async_add_track_to_playlist(track_ids)
|
||||
|
||||
if (
|
||||
enqueue in (MediaPlayerEnqueue.PLAY, MediaPlayerEnqueue.REPLACE)
|
||||
and device.status_code != 4
|
||||
# Move each newly-added track into the desired position
|
||||
for offset, _track_id in enumerate(track_ids):
|
||||
from_index = original_len + offset # position at end after append
|
||||
to_index = insert_at + offset # target position in playlist
|
||||
if from_index > to_index:
|
||||
await device.async_move_track(from_index, to_index)
|
||||
|
||||
if enqueue == MediaPlayerEnqueue.PLAY or (
|
||||
enqueue == MediaPlayerEnqueue.NEXT and device.status_code != STATUS_PLAYING
|
||||
):
|
||||
await device.async_change_track(min(insert_at, original_len))
|
||||
await device.async_play()
|
||||
|
||||
async def async_clear_playlist(self) -> None:
|
||||
@@ -303,3 +371,71 @@ class OasisDeviceMediaPlayerEntity(OasisDeviceEntity, MediaPlayerEntity):
|
||||
"""
|
||||
self.abort_if_busy()
|
||||
await self.device.async_clear_playlist()
|
||||
|
||||
async def async_browse_media(
|
||||
self,
|
||||
media_content_type: MediaType | str | None = None,
|
||||
media_content_id: str | None = None,
|
||||
) -> BrowseMedia:
|
||||
"""
|
||||
Provide a browse tree for Oasis playlists and tracks.
|
||||
|
||||
Root (`None` or oasis_root):
|
||||
- Playlists folder
|
||||
- Tracks folder
|
||||
"""
|
||||
# Root
|
||||
if media_content_id in (None, "", "oasis_root") or media_content_type in (
|
||||
None,
|
||||
MEDIA_TYPE_OASIS_ROOT,
|
||||
):
|
||||
return await build_root_response()
|
||||
|
||||
# Playlists folder
|
||||
if (
|
||||
media_content_type == MEDIA_TYPE_OASIS_PLAYLISTS
|
||||
or media_content_id == "playlists_root"
|
||||
):
|
||||
return await build_playlists_root(self.coordinator.cloud_client)
|
||||
|
||||
# Single playlist
|
||||
if media_content_type == MEDIA_TYPE_OASIS_PLAYLIST:
|
||||
try:
|
||||
playlist_id = int(media_content_id)
|
||||
except (TypeError, ValueError) as err:
|
||||
raise BrowseError(f"Invalid playlist id: {media_content_id}") from err
|
||||
|
||||
return await build_playlist_item(self.coordinator.cloud_client, playlist_id)
|
||||
|
||||
# Tracks folder
|
||||
if (
|
||||
media_content_type == MEDIA_TYPE_OASIS_TRACKS
|
||||
or media_content_id == "tracks_root"
|
||||
):
|
||||
return build_tracks_root()
|
||||
|
||||
# Single track
|
||||
if media_content_type == MEDIA_TYPE_OASIS_TRACK:
|
||||
try:
|
||||
track_id = int(media_content_id)
|
||||
except (TypeError, ValueError) as err:
|
||||
raise BrowseError(f"Invalid track id: {media_content_id}") from err
|
||||
|
||||
return build_track_item(track_id)
|
||||
|
||||
raise BrowseError(
|
||||
f"Unsupported media_content_type/id: {media_content_type}/{media_content_id}"
|
||||
)
|
||||
|
||||
async def async_search_media(
|
||||
self,
|
||||
query: SearchMediaQuery,
|
||||
) -> SearchMedia:
|
||||
"""
|
||||
Search tracks and/or playlists and return a BrowseMedia tree of matches.
|
||||
|
||||
- If media_type == MEDIA_TYPE_OASIS_TRACK: search tracks only
|
||||
- If media_type == MEDIA_TYPE_OASIS_PLAYLIST: search playlists only
|
||||
- Otherwise: search both tracks and playlists
|
||||
"""
|
||||
return await async_search_media(self.coordinator.cloud_client, query)
|
||||
|
||||
Reference in New Issue
Block a user