Skip to content

Commit 1595f6a

Browse files
committed
adjustments for hass integration
1 parent e45aa9b commit 1595f6a

File tree

10 files changed

+94
-256
lines changed

10 files changed

+94
-256
lines changed

music_assistant/constants.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""All constants for Music Assistant."""
22

3-
__version__ = "0.0.30"
3+
__version__ = "0.0.31"
44
REQUIRED_PYTHON_VER = "3.7"
55

66
CONF_USERNAME = "username"
@@ -25,11 +25,10 @@
2525
EVENT_STREAM_STARTED = "streaming started"
2626
EVENT_STREAM_ENDED = "streaming ended"
2727
EVENT_CONFIG_CHANGED = "config changed"
28-
EVENT_PLAYBACK_STARTED = "playback started"
29-
EVENT_PLAYBACK_STOPPED = "playback stopped"
3028
EVENT_MUSIC_SYNC_STATUS = "music sync status"
3129
EVENT_QUEUE_UPDATED = "queue updated"
3230
EVENT_QUEUE_ITEMS_UPDATED = "queue items updated"
31+
EVENT_QUEUE_TIME_UPDATED = "queue time updated"
3332
EVENT_SHUTDOWN = "application shutdown"
3433
EVENT_PROVIDER_REGISTERED = "provider registered"
3534
EVENT_PLAYER_CONTROL_REGISTERED = "player control registered"

music_assistant/http_streamer.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ def __get_audio_stream(
441441
yield (True, b"")
442442
return
443443
# fire event that streaming has started for this track
444+
streamdetails.path = "" # invalidate
444445
self.mass.signal_event(EVENT_STREAM_STARTED, streamdetails)
445446
# yield chunks from stdout
446447
# we keep 1 chunk behind to detect end of stream properly
@@ -462,10 +463,12 @@ def __get_audio_stream(
462463
yield (False, prev_chunk)
463464
prev_chunk = chunk
464465
# fire event that streaming has ended
465-
self.mass.signal_event(EVENT_STREAM_ENDED, streamdetails)
466-
# send task to background to analyse the audio
467-
if queue_item.media_type == MediaType.Track:
468-
self.mass.loop.run_in_executor(None, self.__analyze_audio, streamdetails)
466+
if not cancelled.is_set():
467+
streamdetails.seconds_played = queue_item.duration
468+
self.mass.signal_event(EVENT_STREAM_ENDED, streamdetails)
469+
# send task to background to analyse the audio
470+
if queue_item.media_type == MediaType.Track:
471+
self.mass.add_job(self.__analyze_audio, streamdetails)
469472

470473
def __get_player_sox_options(
471474
self, player_id: str, streamdetails: StreamDetails

music_assistant/models/player.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
class PlayerState(Enum):
1414
"""Enum for the playstate of a player."""
1515

16-
Off = "off"
1716
Stopped = "stopped"
1817
Paused = "paused"
1918
Playing = "playing"
@@ -45,7 +44,7 @@ class Player(DataClassDictMixin):
4544
name: str = ""
4645
powered: bool = False
4746
elapsed_time: int = 0
48-
state: PlayerState = PlayerState.Off
47+
state: PlayerState = PlayerState.Stopped
4948
available: bool = True
5049
current_uri: str = ""
5150
volume_level: int = 0

music_assistant/models/player_queue.py

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
from typing import List
99

1010
from music_assistant.constants import (
11-
EVENT_PLAYBACK_STARTED,
12-
EVENT_PLAYBACK_STOPPED,
1311
EVENT_QUEUE_ITEMS_UPDATED,
12+
EVENT_QUEUE_TIME_UPDATED,
1413
EVENT_QUEUE_UPDATED,
1514
)
1615
from music_assistant.models.media_types import Track
@@ -67,7 +66,6 @@ def __init__(self, mass, player_id):
6766
self._last_item_time = 0
6867
self._last_queue_startindex = 0
6968
self._next_queue_startindex = 0
70-
self._last_player_state = PlayerState.Stopped
7169
self._last_track = None
7270
# load previous queue settings from disk
7371
self.mass.add_job(self.__async_restore_saved_state())
@@ -237,6 +235,7 @@ async def async_next(self):
237235
return
238236
if self.use_queue_stream:
239237
return await self.async_play_index(self.cur_index + 1)
238+
await self.mass.player_manager.async_cmd_power_on(self.player_id)
240239
return await self.mass.player_manager.get_player_provider(
241240
self.player_id
242241
).async_cmd_next(self.player_id)
@@ -245,13 +244,15 @@ async def async_previous(self):
245244
"""Play the previous track in the queue."""
246245
if self.cur_index is None:
247246
return
247+
await self.mass.player_manager.async_cmd_power_on(self.player_id)
248248
if self.use_queue_stream:
249249
return await self.async_play_index(self.cur_index - 1)
250250
return await self.mass.player_manager.async_cmd_previous(self.player_id)
251251

252252
async def async_resume(self):
253253
"""Resume previous queue."""
254254
if self.items:
255+
await self.mass.player_manager.async_cmd_power_on(self.player_id)
255256
prev_index = self.cur_index
256257
supports_queue = PlayerFeature.QUEUE in self.player.features
257258
if self.use_queue_stream or not supports_queue:
@@ -271,6 +272,7 @@ async def async_resume(self):
271272

272273
async def async_play_index(self, index):
273274
"""Play item at index X in queue."""
275+
await self.mass.player_manager.async_cmd_power_on(self.player_id)
274276
player_prov = self.mass.player_manager.get_player_provider(self.player_id)
275277
supports_queue = PlayerFeature.QUEUE in self.player.features
276278
if not isinstance(index, int):
@@ -329,6 +331,7 @@ async def async_move_item(self, queue_item_id, pos_shift=1):
329331

330332
async def async_load(self, queue_items: List[QueueItem]):
331333
"""Load (overwrite) queue with new items."""
334+
await self.mass.player_manager.async_cmd_power_on(self.player_id)
332335
supports_queue = PlayerFeature.QUEUE in self.player.features
333336
for index, item in enumerate(queue_items):
334337
item.sort_index = index
@@ -470,7 +473,6 @@ async def async_update_state(self):
470473
break
471474
# process new index
472475
await self.async_process_queue_update(cur_index, track_time)
473-
self.mass.signal_event(EVENT_QUEUE_UPDATED, self.to_dict())
474476

475477
async def async_start_queue_stream(self):
476478
"""Call when queue_streamer starts playing the queue stream."""
@@ -521,34 +523,21 @@ def __get_queue_stream_index(self):
521523
async def async_process_queue_update(self, new_index, track_time):
522524
"""Compare the queue index to determine if playback changed."""
523525
new_track = self.get_item(new_index)
524-
if (not self._last_track and new_track) or self._last_track != new_track:
526+
self._cur_item_time = track_time
527+
self._cur_index = new_index
528+
if self._last_track != new_track:
525529
# queue track updated
526-
# account for track changing state so trigger track change after 1 second
527-
if self._last_track and self._last_track.streamdetails:
528-
self._last_track.streamdetails.seconds_played = self._last_item_time
529-
self.mass.signal_event(
530-
EVENT_PLAYBACK_STOPPED, self._last_track.streamdetails
531-
)
532-
if new_track and new_track.streamdetails:
533-
self.mass.signal_event(EVENT_PLAYBACK_STARTED, new_track.streamdetails)
534-
self._last_track = new_track
535-
if self._last_player_state != self.player.state:
536-
self._last_player_state = self.player.state
537-
if self.player.elapsed_time == 0 and self.player.state in [
538-
PlayerState.Stopped,
539-
PlayerState.Off,
540-
]:
541-
# player stopped playing
542-
if self._last_track:
543-
self.mass.signal_event(
544-
EVENT_PLAYBACK_STOPPED, self._last_track.streamdetails
545-
)
530+
self._last_track = new_track
531+
self.mass.signal_event(EVENT_QUEUE_UPDATED, self.to_dict())
532+
if self._last_track:
533+
self._last_track.streamdetails = None # invalidate streamdetails
546534
# update vars
547-
if track_time > 2:
548-
# account for track changing state so keep this a few seconds behind
535+
if self._last_item_time != track_time:
549536
self._last_item_time = track_time
550-
self._cur_item_time = track_time
551-
self._cur_index = new_index
537+
self.mass.signal_event(
538+
EVENT_QUEUE_TIME_UPDATED,
539+
{"player_id": self.player_id, "cur_item_time": track_time},
540+
)
552541

553542
@staticmethod
554543
def __shuffle_items(queue_items):

music_assistant/music_manager.py

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
)
2525
from music_assistant.models.musicprovider import MusicProvider
2626
from music_assistant.models.provider import ProviderType
27-
from music_assistant.models.streamdetails import StreamDetails
27+
from music_assistant.models.streamdetails import ContentType, StreamDetails, StreamType
2828
from music_assistant.utils import compare_strings, run_periodic
2929
from PIL import Image
3030

@@ -1077,29 +1077,38 @@ async def async_get_stream_details(
10771077
param media_item: The MediaItem (track/radio) for which to request the streamdetails for.
10781078
param player_id: Optionally provide the player_id which will play this stream.
10791079
"""
1080-
if media_item.streamdetails:
1081-
media_item.streamdetails.player_id = player_id
1082-
return media_item.streamdetails # already present, no need to fetch again!
1083-
# always request the full db track as there might be other qualities available
1084-
# except for radio
1085-
if media_item.media_type == MediaType.Radio:
1086-
full_track = media_item
1087-
else:
1088-
full_track = await self.async_get_track(
1089-
media_item.item_id, media_item.provider, lazy=True, refresh=True
1080+
if media_item.provider == "uri":
1081+
# special type: a plain uri was added to the queue
1082+
streamdetails = StreamDetails(
1083+
type=StreamType.URL,
1084+
provider="uri",
1085+
item_id=media_item.item_id,
1086+
path=media_item.item_id,
1087+
content_type=ContentType(media_item.item_id.split(".")[-1]),
1088+
sample_rate=44100,
1089+
bit_depth=16,
10901090
)
1091-
# sort by quality and check track availability
1092-
for prov_media in sorted(
1093-
full_track.provider_ids, key=lambda x: x.quality, reverse=True
1094-
):
1095-
# get streamdetails from provider
1096-
music_prov = self.mass.get_provider(prov_media.provider)
1097-
if not music_prov:
1098-
continue # provider temporary unavailable ?
1091+
else:
1092+
# always request the full db track as there might be other qualities available
1093+
# except for radio
1094+
if media_item.media_type == MediaType.Radio:
1095+
full_track = media_item
1096+
else:
1097+
full_track = await self.async_get_track(
1098+
media_item.item_id, media_item.provider, lazy=True, refresh=True
1099+
)
1100+
# sort by quality and check track availability
1101+
for prov_media in sorted(
1102+
full_track.provider_ids, key=lambda x: x.quality, reverse=True
1103+
):
1104+
# get streamdetails from provider
1105+
music_prov = self.mass.get_provider(prov_media.provider)
1106+
if not music_prov:
1107+
continue # provider temporary unavailable ?
10991108

1100-
streamdetails = await music_prov.async_get_stream_details(
1101-
prov_media.item_id
1102-
)
1109+
streamdetails = await music_prov.async_get_stream_details(
1110+
prov_media.item_id
1111+
)
11031112

11041113
if streamdetails:
11051114
streamdetails.player_id = player_id

music_assistant/player_manager.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from music_assistant.models.player_queue import PlayerQueue, QueueItem, QueueOption
2525
from music_assistant.models.playerprovider import PlayerProvider
2626
from music_assistant.models.provider import ProviderType
27-
from music_assistant.models.streamdetails import ContentType, StreamDetails, StreamType
2827
from music_assistant.utils import (
2928
async_iter_items,
3029
callback,
@@ -272,18 +271,15 @@ async def async_cmd_play_uri(self, player_id: str, uri: str):
272271
queue_item = QueueItem(
273272
Track(
274273
item_id=uri,
275-
provider="",
276-
name="uri",
274+
provider="uri",
275+
name=uri,
277276
)
278277
)
279-
queue_item.streamdetails = StreamDetails(
280-
type=StreamType.URL,
281-
provider="",
282-
item_id=uri,
283-
path=uri,
284-
content_type=ContentType(uri.split(".")[-1]),
285-
sample_rate=44100,
286-
bit_depth=16,
278+
# generate uri for this queue item
279+
queue_item.uri = "%s/stream/%s/%s" % (
280+
self.mass.web.internal_url,
281+
player_id,
282+
queue_item.queue_item_id,
287283
)
288284
# turn on player
289285
await self.async_cmd_power_on(player_id)
@@ -388,6 +384,22 @@ async def async_cmd_power_off(self, player_id: str) -> None:
388384
for child_player_id in player.group_childs:
389385
if self._players.get(child_player_id):
390386
await self.async_cmd_power_off(child_player_id)
387+
else:
388+
# if this was the last powered player in the group, turn off group
389+
for parent_player_id in player.group_parents:
390+
parent_player = self._players.get(parent_player_id)
391+
if not parent_player:
392+
continue
393+
has_powered_players = False
394+
for child_player_id in parent_player.group_childs:
395+
if child_player_id == player_id:
396+
continue
397+
child_player = self._players.get(child_player_id)
398+
if child_player and child_player.powered:
399+
has_powered_players = True
400+
break
401+
if not has_powered_players:
402+
await self.async_cmd_power_off(parent_player_id)
391403

392404
async def async_cmd_power_toggle(self, player_id: str):
393405
"""
@@ -591,8 +603,6 @@ def __get_player_volume_level(self, player: Player):
591603
@callback
592604
def __get_player_state(self, player: Player, active_parent: str):
593605
"""Get final/calculated player's state."""
594-
if not player.available or not player.powered:
595-
return PlayerState.Off
596606
if active_parent != player.player_id:
597607
# use group state
598608
return self._players[active_parent].state

music_assistant/providers/chromecast/player.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,6 @@ def should_poll(self):
7676
@property
7777
def state(self) -> PlayerState:
7878
"""Return the state of the player."""
79-
if not self._powered:
80-
return PlayerState.Off
8179
if self.media_status is None:
8280
return PlayerState.Stopped
8381
if self.media_status.player_is_playing:

music_assistant/providers/demo/demo_playerprovider.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,17 @@ async def async_cmd_play_uri(self, player_id: str, uri: str):
104104
player.current_uri = uri
105105
player.sox = subprocess.Popen(["play", uri])
106106
player.state = PlayerState.Playing
107+
player.powered = True
107108
self.mass.add_job(self.mass.player_manager.async_update_player(player))
108109

109110
async def report_progress():
110111
"""Report fake progress while sox is playing."""
111112
player.elapsed_time = 0
112-
while player.sox and not player.sox.poll():
113+
while (
114+
player.state == PlayerState.Playing
115+
and player.sox
116+
and not player.sox.poll()
117+
):
113118
await asyncio.sleep(1)
114119
player.elapsed_time += 1
115120
self.mass.add_job(self.mass.player_manager.async_update_player(player))

0 commit comments

Comments
 (0)