Skip to content

Commit

Permalink
Add Player Queue Commands (#65)
Browse files Browse the repository at this point in the history
* Add Get Queue

* Add play queue

* add remove_from_queue

* add save queue

* Add move queue item

* Test save playlsit name too long
  • Loading branch information
andrewsayre authored Jan 8, 2025
1 parent 73ef7ef commit b4103f5
Show file tree
Hide file tree
Showing 12 changed files with 280 additions and 17 deletions.
5 changes: 5 additions & 0 deletions pyheos/command/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@
COMMAND_TOGGLE_MUTE: Final = "player/toggle_mute"
COMMAND_GET_PLAY_MODE: Final = "player/get_play_mode"
COMMAND_SET_PLAY_MODE: Final = "player/set_play_mode"
COMMAND_GET_QUEUE: Final = "player/get_queue"
COMMAND_REMOVE_FROM_QUEUE: Final = "player/remove_from_queue"
COMMAND_CLEAR_QUEUE: Final = "player/clear_queue"
COMMAND_PLAY_QUEUE: Final = "player/play_queue"
COMMAND_SAVE_QUEUE: Final = "player/save_queue"
COMMAND_MOVE_QUEUE_ITEM: Final = "player/move_queue_item"
COMMAND_PLAY_NEXT: Final = "player/play_next"
COMMAND_PLAY_PREVIOUS: Final = "player/play_previous"
COMMAND_PLAY_QUICK_SELECT: Final = "player/play_quickselect"
Expand Down
78 changes: 71 additions & 7 deletions pyheos/command/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@
Define the player command module.
This module creates HEOS player commands.
Commands not currently implemented:
4.2.15 Get Queue
4.2.16 Play Queue Item
4.2.17 Remove Item(s) from Queue
4.2.18 Save Queue as Playlist
4.2.20 Move Queue
"""

from typing import Any

from pyheos import command, const
from pyheos.message import HeosCommand

Expand Down Expand Up @@ -177,6 +172,58 @@ def set_play_mode(
},
)

@staticmethod
def get_queue(
player_id: int, range_start: int | None = None, range_end: int | None = None
) -> HeosCommand:
"""Get the queue for the current player.
References:
4.2.15 Get Queue
"""
params: dict[str, Any] = {const.ATTR_PLAYER_ID: player_id}
if isinstance(range_start, int) and isinstance(range_end, int):
params[const.ATTR_RANGE] = f"{range_start},{range_end}"
return HeosCommand(command.COMMAND_GET_QUEUE, params)

@staticmethod
def play_queue(player_id: int, queue_id: int) -> HeosCommand:
"""Play a queue item.
References:
4.2.16 Play Queue Item"""
return HeosCommand(
command.COMMAND_PLAY_QUEUE,
{const.ATTR_PLAYER_ID: player_id, const.ATTR_QUEUE_ID: queue_id},
)

@staticmethod
def remove_from_queue(player_id: int, queue_ids: list[int]) -> HeosCommand:
"""Remove an item from the queue.
References:
4.2.17 Remove Item(s) from Queue"""
return HeosCommand(
command.COMMAND_REMOVE_FROM_QUEUE,
{
const.ATTR_PLAYER_ID: player_id,
const.ATTR_QUEUE_ID: ",".join(map(str, queue_ids)),
},
)

@staticmethod
def save_queue(player_id: int, name: str) -> HeosCommand:
"""Save the queue as a playlist.
References:
4.2.18 Save Queue as Playlist"""
if len(name) >= 128:
raise ValueError("'name' must be less than or equal to 128 characters")
return HeosCommand(
command.COMMAND_SAVE_QUEUE,
{const.ATTR_PLAYER_ID: player_id, const.ATTR_NAME: name},
)

@staticmethod
def clear_queue(player_id: int) -> HeosCommand:
"""Clear the queue.
Expand All @@ -187,6 +234,23 @@ def clear_queue(player_id: int) -> HeosCommand:
command.COMMAND_CLEAR_QUEUE, {const.ATTR_PLAYER_ID: player_id}
)

@staticmethod
def move_queue_item(
player_id: int, source_queue_ids: list[int], destination_queue_id: int
) -> HeosCommand:
"""Move one or more items in the queue.
References:
4.2.20 Move Queue"""
return HeosCommand(
command.COMMAND_MOVE_QUEUE_ITEM,
{
const.ATTR_PLAYER_ID: player_id,
const.ATTR_SOURCE_QUEUE_ID: ",".join(map(str, source_queue_ids)),
const.ATTR_DESTINATION_QUEUE_ID: destination_queue_id,
},
)

@staticmethod
def play_next(player_id: int) -> HeosCommand:
"""Play next.
Expand Down
2 changes: 2 additions & 0 deletions pyheos/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ATTR_CONTAINER_ID: Final = "cid"
ATTR_COUNT: Final = "count"
ATTR_CURRENT_POSITION: Final = "cur_pos"
ATTR_DESTINATION_QUEUE_ID: Final = "dqid"
ATTR_DURATION: Final = "duration"
ATTR_ENABLE: Final = "enable"
ATTR_ERROR: Final = "error"
Expand Down Expand Up @@ -56,6 +57,7 @@
ATTR_SONG: Final = "song"
ATTR_SOURCE_ID: Final = "sid"
ATTR_SOURCE_PLAYER_ID: Final = "spid"
ATTR_SOURCE_QUEUE_ID: Final = "sqid"
ATTR_SIGNED_OUT: Final = "signed_out"
ATTR_SIGNED_IN: Final = "signed_in"
ATTR_STATE: Final = "state"
Expand Down
65 changes: 58 additions & 7 deletions pyheos/heos.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@
callback_wrapper,
)
from pyheos.error import CommandError, CommandFailedError
from pyheos.media import (
BrowseResult,
MediaItem,
MediaMusicSource,
)
from pyheos.media import BrowseResult, MediaItem, MediaMusicSource, QueueItem
from pyheos.message import HeosMessage
from pyheos.system import HeosHost, HeosSystem

Expand Down Expand Up @@ -717,13 +713,68 @@ async def player_set_play_mode(
PlayerCommands.set_play_mode(player_id, repeat, shuffle)
)

async def player_get_queue(
self,
player_id: int,
range_start: int | None = None,
range_end: int | None = None,
) -> list[QueueItem]:
"""Get the queue for the current player.
References:
4.2.15 Get Queue
"""
result = await self._connection.command(
PlayerCommands.get_queue(player_id, range_start, range_end)
)
payload = cast(list[dict[str, str]], result.payload)
return [QueueItem.from_data(data) for data in payload]

async def player_play_queue(self, player_id: int, queue_id: int) -> None:
"""Play a queue item.
References:
4.2.16 Play Queue Item"""
await self._connection.command(PlayerCommands.play_queue(player_id, queue_id))

async def player_remove_from_queue(
self, player_id: int, queue_ids: list[int]
) -> None:
"""Remove an item from the queue.
References:
4.2.17 Remove Item(s) from Queue"""
await self._connection.command(
PlayerCommands.remove_from_queue(player_id, queue_ids)
)

async def player_save_queue(self, player_id: int, name: str) -> None:
"""Save the queue as a playlist.
References:
4.2.18 Save Queue as Playlist"""
await self._connection.command(PlayerCommands.save_queue(player_id, name))

async def player_clear_queue(self, player_id: int) -> None:
"""Clear the queue.
References:
4.2.19 Clear Queue"""
await self._connection.command(PlayerCommands.clear_queue(player_id))

async def player_move_queue_item(
self, player_id: int, source_queue_ids: list[int], destination_queue_id: int
) -> None:
"""Move one or more items in the queue.
References:
4.2.20 Move Queue"""
await self._connection.command(
PlayerCommands.move_queue_item(
player_id, source_queue_ids, destination_queue_id
)
)

async def player_play_next(self, player_id: int) -> None:
"""Play next.
Expand Down Expand Up @@ -760,7 +811,7 @@ async def player_play_quick_select(
PlayerCommands.play_quick_select(player_id, quick_select_id)
)

async def get_player_quick_selects(self, player_id: int) -> dict[int, str]:
async def player_get_quick_selects(self, player_id: int) -> dict[int, str]:
"""Get quick selects.
References:
Expand All @@ -773,7 +824,7 @@ async def get_player_quick_selects(self, player_id: int) -> dict[int, str]:
for data in cast(list[dict], result.payload)
}

async def check_update(self, player_id: int) -> bool:
async def player_check_update(self, player_id: int) -> bool:
"""Check for a firmware update.
Args:
Expand Down
26 changes: 26 additions & 0 deletions pyheos/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,32 @@
from . import Heos


@dataclass
class QueueItem:
"""Define an item in the queue."""

queue_id: int
song: str
album: str
artist: str
image_url: str
media_id: str
album_id: str

@classmethod
def from_data(cls, data: dict[str, str]) -> "QueueItem":
"""Create a new instance from the provided data."""
return cls(
queue_id=int(data[const.ATTR_QUEUE_ID]),
song=data[const.ATTR_SONG],
album=data[const.ATTR_ALBUM],
artist=data[const.ATTR_ARTIST],
image_url=data[const.ATTR_IMAGE_URL],
media_id=data[const.ATTR_MEDIA_ID],
album_id=data[const.ATTR_ALBUM_ID],
)


@dataclass(init=False)
class Media:
"""
Expand Down
37 changes: 34 additions & 3 deletions pyheos/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import TYPE_CHECKING, Any, Optional, cast

from pyheos.dispatch import DisconnectType, EventCallbackType, callback_wrapper
from pyheos.media import MediaItem
from pyheos.media import MediaItem, QueueItem
from pyheos.message import HeosMessage

from . import const
Expand Down Expand Up @@ -317,11 +317,42 @@ async def set_play_mode(self, repeat: const.RepeatType, shuffle: bool) -> None:
assert self.heos, "Heos instance not set"
await self.heos.player_set_play_mode(self.player_id, repeat, shuffle)

async def get_queue(
self, range_start: int | None = None, range_end: int | None = None
) -> list[QueueItem]:
"""Get the queue of the player."""
assert self.heos, "Heos instance not set"
return await self.heos.player_get_queue(self.player_id, range_start, range_end)

async def play_queue(self, queue_id: int) -> None:
"""Play the queue of the player."""
assert self.heos, "Heos instance not set"
await self.heos.player_play_queue(self.player_id, queue_id)

async def remove_from_queue(self, queue_ids: list[int]) -> None:
"""Remove the specified queue items from the queue."""
assert self.heos, "Heos instance not set"
await self.heos.player_remove_from_queue(self.player_id, queue_ids)

async def clear_queue(self) -> None:
"""Clear the queue of the player."""
assert self.heos, "Heos instance not set"
await self.heos.player_clear_queue(self.player_id)

async def save_queue(self, name: str) -> None:
"""Save the queue as a playlist."""
assert self.heos, "Heos instance not set"
await self.heos.player_save_queue(self.player_id, name)

async def move_queue_item(
self, source_queue_ids: list[int], destination_queue_id: int
) -> None:
"""Move one or more items in the queue."""
assert self.heos, "Heos instance not set"
await self.heos.player_move_queue_item(
self.player_id, source_queue_ids, destination_queue_id
)

async def play_next(self) -> None:
"""Clear the queue of the player."""
assert self.heos, "Heos instance not set"
Expand Down Expand Up @@ -389,12 +420,12 @@ async def set_quick_select(self, quick_select_id: int) -> None:
async def get_quick_selects(self) -> dict[int, str]:
"""Get a list of quick selects."""
assert self.heos, "Heos instance not set"
return await self.heos.get_player_quick_selects(self.player_id)
return await self.heos.player_get_quick_selects(self.player_id)

async def check_update(self) -> bool:
"""Check for a firmware update.
Returns:
True if an update is available, otherwise False."""
assert self.heos, "Heos instance not set"
return await self.heos.check_update(self.player_id)
return await self.heos.player_check_update(self.player_id)
1 change: 1 addition & 0 deletions tests/fixtures/player.get_queue.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"heos": {"command": "player/get_queue", "result": "success", "message": "pid={player_id}&returned=11&count=11"}, "payload": [{"song": "Baby", "album": "22 Break", "artist": "Oh Wonder", "image_url": "http://resources.wimpmusic.com/images/bdfd93c2/0b3a/495e/a557/4493fcbb7ab3/640x640.jpg", "qid": 1, "mid": "199555606", "album_id": "199555605"}, {"song": "Down", "album": "22 Break", "artist": "Oh Wonder", "image_url": "http://resources.wimpmusic.com/images/bdfd93c2/0b3a/495e/a557/4493fcbb7ab3/640x640.jpg", "qid": 2, "mid": "199555607", "album_id": "199555605"}, {"song": "22 Break", "album": "22 Break", "artist": "Oh Wonder", "image_url": "http://resources.wimpmusic.com/images/bdfd93c2/0b3a/495e/a557/4493fcbb7ab3/640x640.jpg", "qid": 3, "mid": "199555608", "album_id": "199555605"}, {"song": "Free", "album": "22 Break", "artist": "Oh Wonder", "image_url": "http://resources.wimpmusic.com/images/bdfd93c2/0b3a/495e/a557/4493fcbb7ab3/640x640.jpg", "qid": 4, "mid": "199555609", "album_id": "199555605"}, {"song": "Don't Let The Neighbourhood Hear", "album": "22 Break", "artist": "Oh Wonder", "image_url": "http://resources.wimpmusic.com/images/bdfd93c2/0b3a/495e/a557/4493fcbb7ab3/640x640.jpg", "qid": 5, "mid": "199555610", "album_id": "199555605"}, {"song": "Dinner", "album": "22 Break", "artist": "Oh Wonder", "image_url": "http://resources.wimpmusic.com/images/bdfd93c2/0b3a/495e/a557/4493fcbb7ab3/640x640.jpg", "qid": 6, "mid": "199555611", "album_id": "199555605"}, {"song": "Rollercoaster Baby", "album": "22 Break", "artist": "Oh Wonder", "image_url": "http://resources.wimpmusic.com/images/bdfd93c2/0b3a/495e/a557/4493fcbb7ab3/640x640.jpg", "qid": 7, "mid": "199555612", "album_id": "199555605"}, {"song": "Love Me Now", "album": "22 Break", "artist": "Oh Wonder", "image_url": "http://resources.wimpmusic.com/images/bdfd93c2/0b3a/495e/a557/4493fcbb7ab3/640x640.jpg", "qid": 8, "mid": "199555613", "album_id": "199555605"}, {"song": "You > Me", "album": "22 Break", "artist": "Oh Wonder", "image_url": "http://resources.wimpmusic.com/images/bdfd93c2/0b3a/495e/a557/4493fcbb7ab3/640x640.jpg", "qid": 9, "mid": "199555614", "album_id": "199555605"}, {"song": "Kicking The Doors Down", "album": "22 Break", "artist": "Oh Wonder", "image_url": "http://resources.wimpmusic.com/images/bdfd93c2/0b3a/495e/a557/4493fcbb7ab3/640x640.jpg", "qid": 10, "mid": "199555615", "album_id": "199555605"}, {"song": "Twenty Fourteen", "album": "22 Break", "artist": "Oh Wonder", "image_url": "http://resources.wimpmusic.com/images/bdfd93c2/0b3a/495e/a557/4493fcbb7ab3/640x640.jpg", "qid": 11, "mid": "199555616", "album_id": "199555605"}]}
1 change: 1 addition & 0 deletions tests/fixtures/player.move_queue_item.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"heos": {"command": "player/move_queue_item", "result": "success", "message": "pid={player_id}&sqid=2,3,4&dqid=1"}}
1 change: 1 addition & 0 deletions tests/fixtures/player.play_queue.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"heos": {"command": "player/play_queue", "result": "success", "message": "pid={player_id}&qid=1"}}
1 change: 1 addition & 0 deletions tests/fixtures/player.remove_from_queue.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"heos": {"command": "player/remove_from_queue", "result": "success", "message": "pid={player_id}&qid=10"}}
1 change: 1 addition & 0 deletions tests/fixtures/player.save_queue.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"heos": {"command": "player/save_queue", "result": "success", "message": "pid={player_id}&name=Test"}}
Loading

0 comments on commit b4103f5

Please sign in to comment.