Skip to content

Commit

Permalink
Add remaining browse commands (#68)
Browse files Browse the repository at this point in the history
* Add get_music_source_info

* Add get search criteria

* Add Search

* Add bounds checking for search param

* Add rename playlist command

* Add delete playlist

* Add retrieve_metadata

* Add service options to BrowseResult

* Add service options to now playing media

* Add set_service_option and happy-path tests

* Param tests

* Refactoring

* Add multi-search

* Optimize some tests

* Add missing exports
  • Loading branch information
andrewsayre authored Jan 9, 2025
1 parent e9e512a commit 274dba4
Show file tree
Hide file tree
Showing 34 changed files with 1,646 additions and 72 deletions.
19 changes: 17 additions & 2 deletions pyheos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,22 @@
from .group import HeosGroup
from .heos import Heos, HeosOptions
from .media import (
AlbumMetadata,
BrowseResult,
ImageMetadata,
Media,
MediaItem,
MediaMusicSource,
QueueItem,
RetreiveMetadataResult,
ServiceOption,
)
from .player import HeosNowPlayingMedia, HeosPlayer, PlayMode
from .search import MultiSearchResult, SearchCriteria, SearchResult, SearchStatistic
from .system import HeosHost, HeosSystem

__all__ = [
"AlbumMetadata",
"BrowseResult",
"CallbackType",
"CommandAuthenticationError",
Expand All @@ -47,15 +54,23 @@
"HeosError",
"HeosGroup",
"HeosHost",
"HeosNowPlayingMedia",
"HeosOptions",
"HeosPlayer",
"HeosNowPlayingMedia",
"HeosSystem",
"ImageMetadata",
"Media",
"MediaItem",
"MediaMusicSource",
"PlayerEventCallbackType",
"MultiSearchResult",
"QueueItem",
"ServiceOption",
"PlayMode",
"PlayerEventCallbackType",
"RetreiveMetadataResult",
"SearchCriteria",
"SearchResult",
"SearchStatistic",
"SendType",
"TargetType",
]
12 changes: 10 additions & 2 deletions pyheos/command/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@
from typing import Final

# Browse commands
COMMAND_BROWSE_GET_SOURCES: Final = "browse/get_music_sources"
COMMAND_BROWSE_ADD_TO_QUEUE: Final = "browse/add_to_queue"
COMMAND_BROWSE_BROWSE: Final = "browse/browse"
COMMAND_BROWSE_DELETE__PLAYLIST: Final = "browse/delete_playlist"
COMMAND_BROWSE_GET_SEARCH_CRITERIA: Final = "browse/get_search_criteria"
COMMAND_BROWSE_GET_SOURCE_INFO: Final = "browse/get_source_info"
COMMAND_BROWSE_GET_SOURCES: Final = "browse/get_music_sources"
COMMAND_BROWSE_MULTI_SEARCH: Final = "browse/multi_search"
COMMAND_BROWSE_PLAY_INPUT: Final = "browse/play_input"
COMMAND_BROWSE_PLAY_PRESET: Final = "browse/play_preset"
COMMAND_BROWSE_PLAY_STREAM: Final = "browse/play_stream"
COMMAND_BROWSE_ADD_TO_QUEUE: Final = "browse/add_to_queue"
COMMAND_BROWSE_RENAME_PLAYLIST: Final = "browse/rename_playlist"
COMMAND_BROWSE_RETRIEVE_METADATA: Final = "browse/retrieve_metadata"
COMMAND_BROWSE_SEARCH: Final = "browse/search"
COMMAND_BROWSE_SET_SERVICE_OPTION: Final = "browse/set_service_option"

# Player commands
COMMAND_GET_PLAYERS: Final = "player/get_players"
Expand Down
298 changes: 287 additions & 11 deletions pyheos/command/browse.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,10 @@
This module creates HEOS browse commands.
Commands not currently implemented:
4.4.2 Get Source Info
4.4.5 Get Source Search Criteria
4.4.6 Search
4.4.14 Rename HEOS Playlist
4.4.15 Delete HEOS Playlist
4.4.17 Retrieve Album Metadata
4.4.19 Set service option
4.4.20 Universal Search (Multi-Search)
Not implemented (commands do not exist/obsolete):
4.4.13 Get HEOS Playlists: Refer to Browse Sources and Browse Source Containers
4.4.16 Get HEOS History: Refer to Browse Sources and Browse Source Containers
4.4.18 Get Service Options for now playing screen: OBSOLETE
"""

from typing import Any
Expand Down Expand Up @@ -60,6 +53,60 @@ def get_music_sources(refresh: bool = False) -> HeosCommand:
params[const.ATTR_REFRESH] = const.VALUE_ON
return HeosCommand(command.COMMAND_BROWSE_GET_SOURCES, params)

@staticmethod
def get_music_source_info(source_id: int) -> HeosCommand:
"""
Create a HEOS command to get information about a music source.
References:
4.4.2 Get Source Info
"""
return HeosCommand(
command.COMMAND_BROWSE_GET_SOURCE_INFO, {const.ATTR_SOURCE_ID: source_id}
)

@staticmethod
def get_search_criteria(source_id: int) -> HeosCommand:
"""
Create a HEOS command to get the search criteria.
References:
4.4.5 Get Search Criteria
"""
return HeosCommand(
command.COMMAND_BROWSE_GET_SEARCH_CRITERIA,
{const.ATTR_SOURCE_ID: source_id},
)

@staticmethod
def search(
source_id: int,
search: str,
criteria_id: int,
range_start: int | None = None,
range_end: int | None = None,
) -> HeosCommand:
"""
Create a HEOS command to search for media.
References:
4.4.6 Search
"""
if search == "":
raise ValueError("'search' parameter must not be empty")
if len(search) > 128:
raise ValueError(
"'search' parameter must be less than or equal to 128 characters"
)
params = {
const.ATTR_SOURCE_ID: source_id,
const.ATTR_SEARCH: search,
const.ATTR_SEARCH_CRITERIA_ID: criteria_id,
}
if isinstance(range_start, int) and isinstance(range_end, int):
params[const.ATTR_RANGE] = f"{range_start},{range_end}"
return HeosCommand(command.COMMAND_BROWSE_SEARCH, params)

@staticmethod
def play_station(
player_id: int,
Expand Down Expand Up @@ -154,3 +201,232 @@ def add_to_queue(
if media_id is not None:
params[const.ATTR_MEDIA_ID] = media_id
return HeosCommand(command.COMMAND_BROWSE_ADD_TO_QUEUE, params)

@staticmethod
def rename_playlist(
source_id: int, container_id: str, new_name: str
) -> HeosCommand:
"""
Create a HEOS command to rename a playlist.
References:
4.4.14 Rename HEOS Playlist
"""
if new_name == "":
raise ValueError("'new_name' parameter must not be empty")
if len(new_name) > 128:
raise ValueError(
"'new_name' parameter must be less than or equal to 128 characters"
)
return HeosCommand(
command.COMMAND_BROWSE_RENAME_PLAYLIST,
{
const.ATTR_SOURCE_ID: source_id,
const.ATTR_CONTAINER_ID: container_id,
const.ATTR_NAME: new_name,
},
)

@staticmethod
def delete_playlist(source_id: int, container_id: str) -> HeosCommand:
"""
Create a HEOS command to delete a playlist.
References:
4.4.15 Delete HEOS Playlist"""
return HeosCommand(
command.COMMAND_BROWSE_DELETE__PLAYLIST,
{const.ATTR_SOURCE_ID: source_id, const.ATTR_CONTAINER_ID: container_id},
)

@staticmethod
def retrieve_metadata(source_it: int, container_id: str) -> HeosCommand:
"""
Create a HEOS command to retrieve metadata.
References:
4.4.17 Retrieve Metadata
"""
return HeosCommand(
command.COMMAND_BROWSE_RETRIEVE_METADATA,
{const.ATTR_SOURCE_ID: source_it, const.ATTR_CONTAINER_ID: container_id},
)

@staticmethod
def set_service_option(
option_id: int,
source_id: int | None,
container_id: str | None,
media_id: str | None,
player_id: int | None,
name: str | None,
criteria_id: int | None,
range_start: int | None = None,
range_end: int | None = None,
) -> HeosCommand:
"""
Create a HEOS command to set a service option.
References:
4.4.19 Set Service Option
"""
params: dict[str, Any] = {const.ATTR_OPTION_ID: option_id}
disallowed_params = {}

if option_id in (
const.SERVICE_OPTION_ADD_TRACK_TO_LIBRARY,
const.SERVICE_OPTION_ADD_STATION_TO_LIBRARY,
const.SERVICE_OPTION_REMOVE_TRACK_FROM_LIBRARY,
const.SERVICE_OPTION_REMOVE_STATION_FROM_LIBRARY,
):
if source_id is None or media_id is None:
raise ValueError(
f"source_id and media_id parameters are required for service option_id {option_id}"
)
disallowed_params = {
"container_id": container_id,
"player_id": player_id,
"name": name,
"criteria_id": criteria_id,
"range_start": range_start,
"range_end": range_end,
}
params[const.ATTR_SOURCE_ID] = source_id
params[const.ATTR_MEDIA_ID] = media_id
elif option_id in (
const.SERVICE_OPTION_ADD_ALBUM_TO_LIBRARY,
const.SERVICE_OPTION_REMOVE_ALBUM_FROM_LIBRARY,
const.SERVICE_OPTION_REMOVE_PLAYLIST_FROM_LIBRARY,
):
if source_id is None or container_id is None:
raise ValueError(
f"source_id and container_id parameters are required for service option_id {option_id}"
)
disallowed_params = {
"media_id": media_id,
"player_id": player_id,
"name": name,
"criteria_id": criteria_id,
"range_start": range_start,
"range_end": range_end,
}
params[const.ATTR_SOURCE_ID] = source_id
params[const.ATTR_CONTAINER_ID] = container_id
elif option_id == const.SERVICE_OPTION_ADD_PLAYLIST_TO_LIBRARY:
if source_id is None or container_id is None or name is None:
raise ValueError(
f"source_id, container_id, and name parameters are required for service option_id {option_id}"
)
disallowed_params = {
"media_id": media_id,
"player_id": player_id,
"criteria_id": criteria_id,
"range_start": range_start,
"range_end": range_end,
}
params[const.ATTR_SOURCE_ID] = source_id
params[const.ATTR_CONTAINER_ID] = container_id
params[const.ATTR_NAME] = name
elif option_id in (
const.SERVICE_OPTION_THUMBS_UP,
const.SERVICE_OPTION_THUMBS_DOWN,
):
if source_id is None or player_id is None:
raise ValueError(
f"source_id and player_id parameters are required for service option_id {option_id}"
)
disallowed_params = {
"media_id": media_id,
"container_id": container_id,
"name": name,
"criteria_id": criteria_id,
"range_start": range_start,
"range_end": range_end,
}
params[const.ATTR_SOURCE_ID] = source_id
params[const.ATTR_PLAYER_ID] = player_id
elif option_id == const.SERVICE_OPTION_CREATE_NEW_STATION_BY_SEARCH_CRITERIA:
if source_id is None or name is None or criteria_id is None:
raise ValueError(
f"source_id, name, and criteria_id parameters are required for service option_id {option_id}"
)
disallowed_params = {
"media_id": media_id,
"container_id": container_id,
"player_id": player_id,
}
params[const.ATTR_SOURCE_ID] = source_id
params[const.ATTR_SEARCH_CRITERIA_ID] = criteria_id
params[const.ATTR_NAME] = name
if isinstance(range_start, int) and isinstance(range_end, int):
params[const.ATTR_RANGE] = f"{range_start},{range_end}"
elif option_id == const.SERVICE_OPTION_ADD_TO_FAVORITES:
if not bool(player_id) ^ (
source_id is not None and media_id is not None and name is not None
):
raise ValueError(
f"Either parameters player_id OR source_id, media_id, and name are required for service option_id {option_id}"
)
if player_id is not None:
if source_id is not None or media_id is not None or name is not None:
raise ValueError(
f"source_id, media_id, and name parameters are not allowed when using player_id for service option_id {option_id}"
)
params[const.ATTR_PLAYER_ID] = player_id
else:
params[const.ATTR_SOURCE_ID] = source_id
params[const.ATTR_MEDIA_ID] = media_id
params[const.ATTR_NAME] = name
disallowed_params = {
"container_id": container_id,
"criteria_id": criteria_id,
"range_start": range_start,
"range_end": range_end,
}
elif option_id == const.SERVICE_OPTION_REMOVE_FROM_FAVORITES:
if media_id is None:
raise ValueError(
f"media_id parameter is required for service option_id {option_id}"
)
params[const.ATTR_MEDIA_ID] = media_id
disallowed_params = {
"source_id": source_id,
"player_id": player_id,
"container_id": container_id,
"name": name,
"criteria_id": criteria_id,
"range_start": range_start,
"range_end": range_end,
}
else:
raise ValueError(f"Unknown option_id: {option_id}")

# Raise if any disallowed parameters are provided
if any(param is not None for param in disallowed_params.values()):
raise ValueError(
f"{', '.join(disallowed_params.keys())} parameters are not allowed for service option_id {option_id}"
)

# return the command
return HeosCommand(command.COMMAND_BROWSE_SET_SERVICE_OPTION, params)

@staticmethod
def multi_search(
search: str, source_ids: list[int] | None, criteria_ids: list[int] | None
) -> HeosCommand:
"""
Create a HEOS command to perform a multi-search.
References:
4.4.20 Multi Search
"""
if len(search) > 128:
raise ValueError(
"'search' parameter must be less than or equal to 128 characters"
)
params = {const.ATTR_SEARCH: search}
if source_ids is not None:
params[const.ATTR_SOURCE_ID] = ",".join(map(str, source_ids))
if criteria_ids is not None:
params[const.ATTR_SEARCH_CRITERIA_ID] = ",".join(map(str, criteria_ids))
return HeosCommand(command.COMMAND_BROWSE_MULTI_SEARCH, params)
2 changes: 1 addition & 1 deletion pyheos/command/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ def save_queue(player_id: int, name: str) -> HeosCommand:
References:
4.2.18 Save Queue as Playlist"""
if len(name) >= 128:
if len(name) > 128:
raise ValueError("'name' must be less than or equal to 128 characters")
return HeosCommand(
command.COMMAND_SAVE_QUEUE,
Expand Down
Loading

0 comments on commit 274dba4

Please sign in to comment.