cd %s
curl -LOks "https://github.com/marcelveldt/musicassistant/archive/master.zip"
unzip -q master.zip
+ pip install -r musicassistant-master/requirements.txt
cp -rf musicassistant-master/music_assistant .
cp -rf musicassistant-master/mass.py .
rm -R musicassistant-master
from .database import Database
from .config import MassConfig
-from .utils import run_periodic, LOGGER, try_parse_bool
+from .utils import run_periodic, LOGGER, try_parse_bool, serialize_values
from .metadata import MetaData
from .cache import Cache
from .music_manager import MusicManager
loop.default_exception_handler(context)
#LOGGER.exception(f"Caught exception: {context}")
- async def signal_event(self, msg, msg_details=None):
+ async def signal_event(self, msg, msg_details:dict):
''' signal (systemwide) event '''
- LOGGER.debug("Event: %s - %s" %(msg, msg_details))
+ if not (msg_details == None or isinstance(msg_details, (str, int, dict))):
+ msg_details = serialize_values(msg_details)
+ LOGGER.debug("Event: %s" %(msg))
listeners = list(self.event_listeners.values())
for callback, eventfilter in listeners:
if not eventfilter or eventfilter in msg:
CONF_KEY_MUSICPROVIDERS = "musicproviders"
CONF_KEY_PLAYERPROVIDERS = "playerproviders"
+EVENT_PLAYER_ADDED = "player added"
+EVENT_PLAYER_REMOVED = "player removed"
EVENT_PLAYER_CHANGED = "player changed"
EVENT_STREAM_STARTED = "streaming started"
EVENT_STREAM_ENDED = "streaming ended"
EVENT_CONFIG_CHANGED = "config changed"
EVENT_PLAYBACK_STARTED = "playback started"
EVENT_PLAYBACK_STOPPED = "playback stopped"
+EVENT_HASS_ENTITY_CHANGED = "hass entity changed"
import json
from .utils import run_periodic, LOGGER, parse_track_title, try_parse_int
from .models.media_types import Track
-from .constants import CONF_ENABLED, CONF_URL, CONF_TOKEN, EVENT_PLAYER_CHANGED
+from .constants import CONF_ENABLED, CONF_URL, CONF_TOKEN, EVENT_PLAYER_CHANGED, EVENT_PLAYER_ADDED, EVENT_HASS_ENTITY_CHANGED
from .cache import use_cache
CONF_KEY = 'homeassistant'
CONF_PUBLISH_PLAYERS = "publish_players"
-EVENT_HASS_CHANGED = "hass entity changed"
### auto detect hassio for auto config ####
if os.path.isfile('/data/options.json'):
loop=self.mass.event_loop, connector=aiohttp.TCPConnector())
self.mass.event_loop.create_task(self.__hass_websocket())
await self.mass.add_event_listener(self.mass_event, EVENT_PLAYER_CHANGED)
+ await self.mass.add_event_listener(self.mass_event, EVENT_PLAYER_ADDED)
self.mass.event_loop.create_task(self.__get_sources())
async def get_state_async(self, entity_id, attribute='state'):
if 'state' in state_obj:
self._tracked_entities[entity_id] = state_obj
self.mass.event_loop.create_task(
- self.mass.signal_event(EVENT_HASS_CHANGED, entity_id))
+ self.mass.signal_event(EVENT_HASS_ENTITY_CHANGED, state_obj))
async def mass_event(self, msg, msg_details):
''' received event from mass '''
- if msg == EVENT_PLAYER_CHANGED:
+ if msg in [EVENT_PLAYER_CHANGED, EVENT_PLAYER_ADDED]:
await self.publish_player(msg_details)
async def hass_event(self, event_type, event_data):
if event_data['entity_id'] in self._tracked_entities:
self._tracked_entities[event_data['entity_id']] = event_data['new_state']
self.mass.event_loop.create_task(
- self.mass.signal_event(EVENT_HASS_CHANGED, event_data['entity_id']))
+ self.mass.signal_event(EVENT_HASS_ENTITY_CHANGED, event_data))
elif event_type == 'call_service' and event_data['domain'] == 'media_player':
await self.__handle_player_command(event_data['service'], event_data['service_data'])
track.provider = 'http'
return await self.mass.players.play_media(player_id, track, queue_opt)
- async def publish_player(self, player):
+ async def publish_player(self, player_info):
''' publish player details to hass'''
if not self.mass.config['base']['homeassistant']['publish_players']:
return False
- player_id = player.player_id
- entity_id = 'media_player.mass_' + slug.slugify(player.name, separator='_').lower()
- state = player.state if player.powered else 'off'
+ if not player_info["name"]:
+ return
+ # TODO: throttle updates to home assistant ?
+ player_id = player_info["player_id"]
+ entity_id = 'media_player.mass_' + slug.slugify(player_info["name"], separator='_').lower()
+ state = player_info["state"]
state_attributes = {
"supported_features": 65471,
- "friendly_name": player.name,
+ "friendly_name": player_info["name"],
"source_list": self._sources,
"source": 'unknown',
- "volume_level": player.volume_level/100,
- "is_volume_muted": player.muted,
- "media_duration": player.cur_item.duration if player.cur_item else 0,
- "media_position": player.cur_time,
- "media_title": player.cur_item.name if player.cur_item else "",
- "media_artist": player.cur_item.artists[0].name if player.cur_item and player.cur_item.artists else "",
- "media_album_name": player.cur_item.album.name if player.cur_item and player.cur_item.album else "",
- "entity_picture": player.cur_item.album.metadata.get('image') if player.cur_item and player.cur_item.album else ""
+ "volume_level": player_info["volume_level"]/100,
+ "is_volume_muted": player_info["muted"],
+ "media_position_updated_at": player_info["media_position_updated_at"],
+ "media_duration": None,
+ "media_position": player_info["cur_time"],
+ "media_title": None,
+ "media_artist": None,
+ "media_album_name": None,
+ "entity_picture": None
}
+ if state != "off":
+ player = await self.mass.players.get_player(player_id)
+ if player.queue.cur_item:
+ queue_item = await player.queue.by_item_id(player.queue.cur_item)
+ state_attributes["media_duration"] = queue_item.duration
+ state_attributes["media_title"] = queue_item.name
+ state_attributes["media_artist"] = queue_item.artists[0].name
+ state_attributes["media_album_name"] = queue_item.album.name
+ state_attributes["entity_picture"] = queue_item.album.metadata.get("image")
self._published_players[entity_id] = player_id
await self.__set_state(entity_id, state, state_attributes)
sox_proc.terminate()
fill_buffer_thread.join()
del sox_proc
+ # run garbage collect manually to avoid too much memory fragmentation
+ gc.collect()
LOGGER.info("streaming of queue for player %s completed" % player.name)
async def __get_audio_stream(self, player, queue_item, cancelled,
self.mass.event_loop).result()
if streamdetails:
streamdetails['player_id'] = player.player_id
+ if not 'item_id' in streamdetails:
+ streamdetails['item_id'] = prov_media['item_id']
+ if not 'provider' in streamdetails:
+ streamdetails['provider'] = prov_media['provider']
+ if not 'quality' in streamdetails:
+ streamdetails['quality'] = prov_media['quality']
queue_item.streamdetails = streamdetails
- queue_item.item_id = prov_media['item_id']
- queue_item.provider = prov_media['provider']
- queue_item.quality = prov_media['quality']
break
if not streamdetails:
LOGGER.warning(f"no stream details for {queue_item.name}")
yield (True, b'')
return
# get sox effects and resample options
- sox_options = await self.__get_player_sox_options(player, queue_item)
+ sox_options = await self.__get_player_sox_options(player, streamdetails)
outputfmt = 'flac -C 0'
if resample:
outputfmt = 'raw -b 32 -c 2 -e signed-integer'
process = subprocess.Popen(args, shell=True, stdout=subprocess.PIPE)
# fire event that streaming has started for this track
asyncio.run_coroutine_threadsafe(
- self.mass.signal_event(EVENT_STREAM_STARTED, queue_item), self.mass.event_loop)
+ self.mass.signal_event(EVENT_STREAM_STARTED, streamdetails), self.mass.event_loop)
# yield chunks from stdout
# we keep 1 chunk behind to detect end of stream properly
bytes_sent = 0
# fire event that streaming has ended
asyncio.run_coroutine_threadsafe(
- self.mass.signal_event(EVENT_STREAM_ENDED, queue_item), self.mass.event_loop)
+ self.mass.signal_event(EVENT_STREAM_ENDED, streamdetails), self.mass.event_loop)
# send task to main event loop to analyse the audio
- self.mass.event_loop.call_soon_threadsafe(
- asyncio.ensure_future, self.__analyze_audio(queue_item))
+ if queue_item.media_type == MediaType.Track:
+ self.mass.event_loop.call_soon_threadsafe(
+ asyncio.ensure_future, self.__analyze_audio(streamdetails))
# run garbage collect manually to avoid too much memory fragmentation
gc.collect()
- async def __get_player_sox_options(self, player, queue_item):
+ async def __get_player_sox_options(self, player, streamdetails):
''' get player specific sox effect options '''
sox_options = []
# volume normalisation
gain_correct = asyncio.run_coroutine_threadsafe(
self.mass.players.get_gain_correct(
- player.player_id, queue_item.item_id, queue_item.provider),
+ player.player_id, streamdetails["item_id"], streamdetails["provider"]),
self.mass.event_loop).result()
if gain_correct != 0:
sox_options.append('vol %s dB ' % gain_correct)
if player.settings['max_sample_rate']:
max_sample_rate = try_parse_int(player.settings['max_sample_rate'])
if max_sample_rate:
- quality = queue_item.quality
+ quality = streamdetails["quality"]
if quality > TrackQuality.FLAC_LOSSLESS_HI_RES_3 and max_sample_rate == 192000:
sox_options.append('rate -v 192000')
elif quality > TrackQuality.FLAC_LOSSLESS_HI_RES_2 and max_sample_rate == 96000:
sox_options.append(player.settings['sox_options'])
return " ".join(sox_options)
- async def __analyze_audio(self, queue_item):
+ async def __analyze_audio(self, streamdetails):
''' analyze track audio, for now we only calculate EBU R128 loudness '''
- if queue_item.media_type != MediaType.Track:
- # TODO: calculate loudness average for web radio ?
- LOGGER.debug("analyze is only supported for tracks")
- return
- item_key = '%s%s' %(queue_item.item_id, queue_item.provider)
+ item_key = '%s%s' %(streamdetails["item_id"], streamdetails["provider"])
if item_key in self.analyze_jobs:
return # prevent multiple analyze jobs for same track
self.analyze_jobs[item_key] = True
- streamdetails = queue_item.streamdetails
track_loudness = await self.mass.db.get_track_loudness(
- queue_item.item_id, queue_item.provider)
+ streamdetails["item_id"], streamdetails["provider"])
if track_loudness == None:
# only when needed we do the analyze stuff
LOGGER.debug('Start analyzing track %s' % item_key)
meter = pyloudnorm.Meter(rate) # create BS.1770 meter
loudness = meter.integrated_loudness(data) # measure loudness
del data
- await self.mass.db.set_track_loudness(queue_item.item_id, queue_item.provider, loudness)
+ await self.mass.db.set_track_loudness(streamdetails["item_id"], streamdetails["provider"], loudness)
del audio_data
LOGGER.debug("Integrated loudness of track %s is: %s" %(item_key, loudness))
self.analyze_jobs.pop(item_key, None)
from enum import Enum
from typing import List
import operator
-from ..utils import run_periodic, LOGGER, parse_track_title, try_parse_int, try_parse_bool, try_parse_float
+import time
+from ..utils import run_periodic, LOGGER, parse_track_title, try_parse_int, \
+ try_parse_bool, try_parse_float
from ..constants import EVENT_PLAYER_CHANGED
from ..cache import use_cache
from .media_types import Track, MediaType
#### Common implementation, should NOT be overrridden #####
- def __init__(self, mass, player_id, prov_id):
+ def __init__(self, mass, player_id, prov_id, is_group=False):
# private attributes
self.mass = mass
self._player_id = player_id # unique id for this player
self._prov_id = prov_id # unique provider id for the player
self._name = ''
- self._is_group = False
- self._state = PlayerState.Stopped
+ self._state = PlayerState.Stopped
+ self._group_childs = []
+ self._last_group_parent = None
self._powered = False
self._cur_time = 0
+ self._media_position_updated_at = 0
self._cur_uri = ''
self._volume_level = 0
self._muted = False
- self._group_parent = None
self._queue = PlayerQueue(mass, self)
self._player_settings = None
+ self._initialized = False
+ self._last_event = 0
+ self._update_cur_time_task = None
# public attributes
self.supports_queue = True # has native support for a queue
self.supports_gapless = False # has native gapless support
self.supports_crossfade = False # has native crossfading support
- # if home assistant support is enabled, register state listener
- if self.mass.hass.enabled:
- self.mass.event_loop.create_task(
- self.mass.add_event_listener(self.hass_state_listener, "hass entity changed"))
+
+
+ def __del__(self):
+ if self._update_cur_time_task:
+ self._update_cur_time_task.cancel()
@property
def player_id(self):
@property
def is_group(self):
''' [PROTECTED] is_group property of this player '''
- return self._is_group
+ return len(self._group_childs) > 0
- @is_group.setter
- def is_group(self, is_group:bool):
- ''' [PROTECTED] set is_group property of this player '''
- if is_group != self._is_group:
- self._is_group = is_group
+ @property
+ def group_parents(self):
+ ''' [PROTECTED] player ids of all groups this player belongs to '''
+ player_ids = []
+ for item in self.mass.players._players.values():
+ if self.player_id in item.group_childs:
+ player_ids.append(item.player_id)
+ return player_ids
+
+ @property
+ def group_childs(self)->list:
+ '''
+ [PROTECTED]
+ return all child player ids for this group player as list
+ empty list if this player is not a group player
+ '''
+ return self._group_childs
+
+ @group_childs.setter
+ def group_childs(self, group_childs:list):
+ ''' [PROTECTED] set group_childs property of this player '''
+ if group_childs != self._group_childs:
+ self._group_childs = group_childs
+ self.mass.event_loop.create_task(self.update())
+ for child_player_id in group_childs:
+ self.mass.event_loop.create_task(
+ self.mass.players.trigger_update(child_player_id))
+
+ def add_group_child(self, child_player_id):
+ ''' add player as child to this group player '''
+ if not child_player_id in self._group_childs:
+ self._group_childs.append(child_player_id)
self.mass.event_loop.create_task(self.update())
+ self.mass.event_loop.create_task(
+ self.mass.players.trigger_update(child_player_id))
+
+ def remove_group_child(self, child_player_id):
+ ''' remove player as child from this group player '''
+ if child_player_id in self._group_childs:
+ self._group_childs.remove(child_player_id)
+ self.mass.event_loop.create_task(self.update())
+ self.mass.event_loop.create_task(
+ self.mass.players.trigger_update(child_player_id))
@property
def state(self):
''' [PROTECTED] state property of this player '''
if not self.powered:
return PlayerState.Off
- if self.group_parent:
- group_player = self.mass.players._players.get(self.group_parent)
- if group_player:
+ # prefer group player state
+ for group_parent_id in self.group_parents:
+ group_player = self.mass.players.get_player_sync(group_parent_id)
+ if group_player and group_player.state != PlayerState.Off:
+ self._last_group_parent = group_parent_id
return group_player.state
return self._state
''' [PROTECTED] set state property of this player '''
if state != self._state:
self._state = state
- self.mass.event_loop.create_task(self.update())
+ self.mass.event_loop.create_task(self.update(update_queue=True))
@property
def powered(self):
''' [PROTECTED] set (real) power state for this player '''
if powered != self._powered:
self._powered = powered
- self.mass.event_loop.create_task(self.update())
+ self.mass.event_loop.create_task(self.update())
@property
def cur_time(self):
''' [PROTECTED] cur_time (player's elapsed time) property of this player '''
- # handle group player
- if self.group_parent:
- group_player = self.mass.players.get_player_sync(self.group_parent)
- if group_player:
+ # prefer group player state
+ for group_id in self.group_parents:
+ group_player = self.mass.players.get_player_sync(group_id)
+ if group_player.state != PlayerState.Off:
return group_player.cur_time
return self.queue.cur_item_time
@cur_time.setter
def cur_time(self, cur_time:int):
''' [PROTECTED] set cur_time (player's elapsed time) property of this player '''
+ if cur_time == None:
+ cur_time = 0
if cur_time != self._cur_time:
self._cur_time = cur_time
- self.mass.event_loop.create_task(self.update())
+ self._media_position_updated_at = time.time()
+ self.mass.event_loop.create_task(self.update(update_queue=True))
+
+ @property
+ def media_position_updated_at(self):
+ ''' [PROTECTED] When was the position of the current playing media valid. '''
+ return self._media_position_updated_at
@property
def cur_uri(self):
''' [PROTECTED] cur_uri (uri loaded in player) property of this player '''
- # handle group player
- if self.group_parent:
- group_player = self.mass.players.get_player_sync(self.group_parent)
- if group_player:
+ # prefer group player's state
+ for group_id in self.group_parents:
+ group_player = self.mass.players.get_player_sync(group_id)
+ if group_player.state != PlayerState.Off:
return group_player.cur_uri
return self._cur_uri
''' [PROTECTED] set cur_uri (uri loaded in player) property of this player '''
if cur_uri != self._cur_uri:
self._cur_uri = cur_uri
- self.mass.event_loop.create_task(self.update())
+ self.mass.event_loop.create_task(self.update(update_queue=True))
@property
def volume_level(self):
if self.is_group:
group_volume = 0
active_players = 0
- for child_player in self.group_childs:
- if child_player.enabled and child_player.powered:
+ for child_player_id in self.group_childs:
+ child_player = self.mass.players._players.get(child_player_id)
+ if child_player and child_player.enabled and child_player.powered:
group_volume += child_player.volume_level
active_players += 1
if active_players:
if volume_level != self._volume_level:
self._volume_level = volume_level
self.mass.event_loop.create_task(self.update())
+ # trigger update on group player
+ for group_parent_id in self.group_parents:
+ self.mass.event_loop.create_task(
+ self.mass.players.trigger_update(group_parent_id))
@property
def muted(self):
self._muted = is_muted
self.mass.event_loop.create_task(self.update())
- @property
- def group_parent(self):
- ''' [PROTECTED] group_parent property of this player '''
- return self._group_parent
-
- @group_parent.setter
- def group_parent(self, group_parent:str):
- ''' [PROTECTED] set muted property of this player '''
- if group_parent != self._group_parent:
- self._group_parent = group_parent
- self.mass.event_loop.create_task(self.update())
-
- @property
- def group_childs(self):
- ''' [PROTECTED] return group childs '''
- if not self.is_group:
- return []
- return [item for item in self.mass.players.players if item.group_parent == self.player_id]
-
@property
def enabled(self):
''' [PROTECTED] player enabled config setting '''
@property
def queue(self):
''' [PROTECTED] player's queue '''
- # handle group player
- if self.group_parent:
- group_player = self.mass.players.get_player_sync(self.group_parent)
- if group_player:
+ # prefer group player's state
+ for group_id in self.group_parents:
+ group_player = self.mass.players.get_player_sync(group_id)
+ if group_player.state != PlayerState.Off:
return group_player.queue
return self._queue
- @property
- def cur_item(self):
- ''' current item in the player's queue '''
- return self.queue.cur_item
-
async def stop(self):
''' [PROTECTED] send stop command to player '''
- if self.group_parent:
- # redirect playback related commands to parent player
- group_player = await self.mass.players.get_player(self.group_parent)
- if group_player:
+ # redirect playback related commands to parent player
+ for group_id in self.group_parents:
+ group_player = self.mass.players.get_player_sync(group_id)
+ if group_player.state != PlayerState.Off:
return await group_player.stop()
- else:
- return await self.cmd_stop()
+ return await self.cmd_stop()
async def play(self):
''' [PROTECTED] send play (unpause) command to player '''
- if self.group_parent:
- # redirect playback related commands to parent player
- group_player = await self.mass.players.get_player(self.group_parent)
- if group_player:
+ # redirect playback related commands to parent player
+ for group_id in self.group_parents:
+ group_player = self.mass.players.get_player_sync(group_id)
+ if group_player.state != PlayerState.Off:
return await group_player.play()
- elif self.state == PlayerState.Paused:
+ if self.state == PlayerState.Paused:
return await self.cmd_play()
elif self.state != PlayerState.Playing:
return await self.queue.resume()
async def pause(self):
''' [PROTECTED] send pause command to player '''
- if self.group_parent:
- # redirect playback related commands to parent player
- group_player = await self.mass.players.get_player(self.group_parent)
- if group_player:
+ # redirect playback related commands to parent player
+ for group_id in self.group_parents:
+ group_player = self.mass.players.get_player_sync(group_id)
+ if group_player.state != PlayerState.Off:
return await group_player.pause()
- else:
- return await self.cmd_pause()
+ return await self.cmd_pause()
async def play_pause(self):
''' toggle play/pause'''
async def next(self):
''' [PROTECTED] send next command to player '''
- if self.group_parent:
- # redirect playback related commands to parent player
- group_player = await self.mass.players.get_player(self.group_parent)
- if group_player:
+ # redirect playback related commands to parent player
+ for group_id in self.group_parents:
+ group_player = self.mass.players.get_player_sync(group_id)
+ if group_player.state != PlayerState.Off:
return await group_player.next()
- else:
- return await self.queue.next()
+ return await self.queue.next()
async def previous(self):
''' [PROTECTED] send previous command to player '''
- if self.group_parent:
- # redirect playback related commands to parent player
- group_player = await self.mass.players.get_player(self.group_parent)
- if group_player:
+ # redirect playback related commands to parent player
+ for group_id in self.group_parents:
+ group_player = self.mass.players.get_player_sync(group_id)
+ if group_player.state != PlayerState.Off:
return await group_player.previous()
- else:
- return await self.queue.previous()
+ return await self.queue.previous()
async def power(self, power):
''' [PROTECTED] send power ON command to player '''
domain = self.settings['hass_power_entity'].split('.')[0]
service_data = { 'entity_id': self.settings['hass_power_entity']}
await self.mass.hass.call_service(domain, 'turn_on', service_data)
+ # power on group parent if needed
+ last_group_player = await self.mass.players.get_player(self._last_group_parent)
+ if last_group_player:
+ await last_group_player.power_on()
# handle play on power on
- if self.settings.get('play_power_on'):
+ elif self.settings.get('play_power_on'):
await self.play()
- # handle group power
- if self.group_parent:
- # player has a group parent, check if it should be turned on
- group_player = await self.mass.players.get_player(self.group_parent)
- if group_player and not group_player.powered:
- return await group_player.power_on()
async def power_off(self):
- ''' [PROTECTED] send power TOGGLE command to player '''
+ ''' [PROTECTED] send power OFF command to player '''
+ if self._state in [PlayerState.Playing, PlayerState.Paused]:
+ await self.stop()
await self.cmd_power_off()
# handle mute as power
if self.settings.get('mute_as_power'):
# handle group power
if self.is_group:
# player is group, turn off all childs
- for item in self.group_childs:
- if item.powered:
- await item.power_off()
- elif self.group_parent:
- # player has a group parent, check if it should be turned off
- group_player = await self.mass.players.get_player(self.group_parent)
- if group_player.powered:
+ for child_player_id in self.group_childs:
+ child_player = self.mass.players._players.get(child_player_id)
+ if child_player and child_player.powered:
+ await child_player.power_off()
+ # if player has group parent(s), check if it should be turned off
+ for group_parent_id in self.group_parents:
+ group_player = await self.mass.players.get_player(group_parent_id)
+ if group_player.state != PlayerState.Off:
needs_power = False
- for child_player in group_player.group_childs:
- if child_player.player_id != self.player_id and child_player.powered:
+ for child_player_id in group_player.group_childs:
+ if child_player_id == self.player_id:
+ continue
+ child_player = self.mass.players._players.get(child_player_id)
+ if child_player and child_player.powered:
needs_power = True
break
if not needs_power:
volume_dif_percent = 1+(new_volume/100)
else:
volume_dif_percent = volume_dif/cur_volume
- for child_player in self.group_childs:
- if child_player.enabled and child_player.powered:
+ for child_player_id in self.group_childs:
+ child_player = self.mass.players._players.get(child_player_id)
+ if child_player and child_player.enabled and child_player.powered:
cur_child_volume = child_player.volume_level
new_child_volume = cur_child_volume + (cur_child_volume * volume_dif_percent)
await child_player.volume_set(new_child_volume)
''' [PROTECTED] send mute command to player '''
return await self.cmd_volume_mute(is_muted)
- async def update(self):
+ async def update(self, update_queue=False):
''' [PROTECTED] signal player updated '''
- await self.queue.update()
- await self.mass.signal_event(EVENT_PLAYER_CHANGED, self)
self.get_player_settings()
-
- async def hass_state_listener(self, msg, msg_details=None):
- ''' called when tracked entities in hass change state '''
- if (msg_details == self.settings.get('hass_power_entity') or
- msg_details == self.settings.get('hass_volume_entity')):
- await self.update()
+ if not self._initialized:
+ return
+ # update queue state if player state changes
+ if update_queue:
+ await self.queue.update()
+ await self.mass.signal_event(EVENT_PLAYER_CHANGED, self.to_dict())
+ if self._state == PlayerState.Playing and not self._update_cur_time_task and (time.time() - self._media_position_updated_at > 2):
+ self._update_cur_time_task = self.mass.event_loop.create_task(self.__update_cur_time())
+
+ async def __update_cur_time(self):
+ ''' background task that keeps updating the current time '''
+ while self._state == PlayerState.Playing:
+ calc_time = self._cur_time + (time.time() - self._media_position_updated_at)
+ self.cur_time = calc_time
+ await asyncio.sleep(1)
+ self._update_cur_time_task = None
@property
def settings(self):
"state": self.state,
"powered": self.powered,
"cur_time": self.cur_time,
+ "media_position_updated_at": self.media_position_updated_at,
"cur_uri": self.cur_uri,
"volume_level": self.volume_level,
"muted": self.muted,
- "group_parent": self.group_parent,
+ "group_parents": self.group_parents,
+ "group_childs": self.group_childs,
"enabled": self.enabled,
- "cur_item": self.cur_item.__dict__ if self.cur_item else None
+ "cur_queue_index": self.queue.cur_index,
+ "cur_queue_item": self.queue.cur_item
}
\ No newline at end of file
@property
def cur_index(self):
''' match current uri with queue items to determine queue index '''
+ if not self._items:
+ return None
return self._cur_index
@property
def cur_item(self):
+ ''' return the queue item id of the current item in the queue '''
if self.cur_index == None or not len(self.items) > self.cur_index:
return None
- return self.items[self.cur_index]
+ return self.items[self.cur_index].queue_item_id
@property
def cur_item_time(self):
async def next(self):
''' request next track in queue '''
+ if self.cur_index == None:
+ return
if self.use_queue_stream:
return await self.play_index(self.cur_index+1)
else:
async def previous(self):
''' request previous track in queue '''
+ if self.cur_index == None:
+ return
if self.use_queue_stream:
return await self.play_index(self.cur_index-1)
else:
if (not self._last_track and new_track) or self._last_track != new_track:
# queue track updated
# account for track changing state so trigger track change after 1 second
- if self._last_track:
- self._last_track.seconds_played = self._last_item_time
+ if self._last_track and self._last_track.streamdetails:
+ self._last_track.streamdetails["seconds_played"] = self._last_item_time
self.mass.event_loop.create_task(
- self.mass.signal_event(EVENT_PLAYBACK_STOPPED, self._last_track))
+ self.mass.signal_event(EVENT_PLAYBACK_STOPPED, self._last_track.streamdetails))
if new_track:
self.mass.event_loop.create_task(
- self.mass.signal_event(EVENT_PLAYBACK_STARTED, new_track))
+ self.mass.signal_event(EVENT_PLAYBACK_STARTED, new_track.streamdetails))
self._last_track = new_track
await self.__save_to_file()
if self._last_player_state != self._player.state:
Stopped = "stopped"
Paused = "paused"
Playing = "playing"
-
- # def from_string(self, string):
- # if string == "off":
- # return self.Off
- # elif string == "stopped":
- # return self.Stopped
- # elif string == "paused":
- # return self.Paused
- # elif string == "playing":
- # return self.Playing
if not self.__user_auth_info:
return
# TODO: need to figure out if the streamed track is purchased by user
- if msg == EVENT_PLAYBACK_STARTED and msg_details.provider == self.prov_id:
+ if msg == EVENT_PLAYBACK_STARTED and msg_details["provider"] == self.prov_id:
# report streaming started to qobuz
device_id = self.__user_auth_info["user"]["device"]["id"]
credential_id = self.__user_auth_info["user"]["credential"]["id"]
user_id = self.__user_auth_info["user"]["id"]
- format_id = msg_details.streamdetails["details"]["format_id"]
+ format_id = msg_details["details"]["format_id"]
timestamp = int(time.time())
events=[{"online": True, "sample": False, "intent": "stream", "device_id": device_id,
- "track_id": msg_details.item_id, "purchase": False, "date": timestamp,
+ "track_id": msg_details["item_id"], "purchase": False, "date": timestamp,
"credential_id": credential_id, "user_id": user_id, "local": False, "format_id":format_id}]
await self.__post_data("track/reportStreamingStart", data=events)
- elif msg == EVENT_PLAYBACK_STOPPED and msg_details.provider == self.prov_id:
+ elif msg == EVENT_PLAYBACK_STOPPED and msg_details["provider"] == self.prov_id:
# report streaming ended to qobuz
user_id = self.__user_auth_info["user"]["id"]
params = {
'user_id': user_id,
- 'track_id': msg_details.item_id,
- 'duration': int(msg_details.seconds_played)
+ 'track_id': msg_details["item_id"],
+ 'duration': int(msg_details["seconds_played"])
}
await self.__get_data('/track/reportStreamingEnd', params)
import functools
import urllib
-from .constants import CONF_KEY_PLAYERPROVIDERS
+from .constants import CONF_KEY_PLAYERPROVIDERS, EVENT_PLAYER_ADDED, EVENT_PLAYER_REMOVED, EVENT_HASS_ENTITY_CHANGED
from .utils import run_periodic, LOGGER, try_parse_int, try_parse_float, \
get_ip, run_async_background_task, load_provider_modules
from .models.media_types import MediaType, TrackQuality
# start providers
for prov in self.providers.values():
await prov.setup()
+ # register state listener
+ await self.mass.add_event_listener(self.handle_mass_events, EVENT_HASS_ENTITY_CHANGED)
@property
def players(self):
async def add_player(self, player):
''' register a new player '''
+ player._initialized = True
self._players[player.player_id] = player
- await self.mass.signal_event('player added', player)
+ await self.mass.signal_event(EVENT_PLAYER_ADDED, player.to_dict())
# TODO: turn on player if it was previously turned on ?
LOGGER.info(f"New player added: {player.player_provider}/{player.player_id}")
return player
async def remove_player(self, player_id):
''' handle a player remove '''
self._players.pop(player_id, None)
- await self.mass.signal_event('player removed', player_id)
+ await self.mass.signal_event(EVENT_PLAYER_REMOVED, {"player_id": player_id})
LOGGER.info(f"Player removed: {player_id}")
async def trigger_update(self, player_id):
elif queue_opt == 'add':
return await player.queue.append(queue_items)
+ async def handle_mass_events(self, msg, msg_details=None):
+ ''' listen to some events on event bus '''
+ if msg == EVENT_HASS_ENTITY_CHANGED:
+ # handle players with hass integration enabled
+ player_ids = list(self._players.keys())
+ for player_id in player_ids:
+ player = self._players[player_id]
+ if (msg_details['entity_id'] == player.settings.get('hass_power_entity') or
+ msg_details['entity_id'] == player.settings.get('hass_volume_entity')):
+ await player.update()
+
async def get_gain_correct(self, player_id, item_id, provider_id, replaygain=False):
''' get gain correction for given player / track combination '''
player = self._players[player_id]
class ChromecastPlayer(Player):
''' Chromecast player object '''
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._poll_task = self.mass.event_loop.create_task(self.__poll_status())
+
+ def __del__(self):
+ if self._poll_task:
+ self._poll_task.cancel()
+
async def try_chromecast_command(self, cmd:types.MethodType, *args, **kwargs):
''' guard for disconnected socket client '''
def _try_chromecast_command(_cmd:types.MethodType, *_args, **_kwargs):
async def cmd_power_off(self):
''' send power OFF command to player '''
self.powered = False
- # power is not supported so send quit_app instead
- if not self.group_parent:
- await self.try_chromecast_command(self.cc.quit_app)
async def cmd_volume_set(self, volume_level):
''' send new volume level command to player '''
else:
send_queue()
+ @run_periodic(10)
+ async def __poll_status(self):
+ ''' request actual status from CC '''
+ # this is needed to get some accurate media progress info
+ if self._state == PlayerState.Playing:
+ await self.try_chromecast_command(self.cc.media_controller.update_status)
+
async def handle_player_state(self, caststatus=None,
mediastatus=None, connection_status=None):
''' handle a player state message from the socket '''
self.state = PlayerState.Stopped
self.cur_uri = mediastatus.content_id
self.cur_time = mediastatus.adjusted_current_time
- # create update/poll task for the current time
- async def poll_task():
- self.poll_task = True
- while self.state == PlayerState.Playing:
- self.cur_time = mediastatus.adjusted_current_time
- await asyncio.sleep(1)
- self.poll_task = False
- if not self.poll_task and self.state == PlayerState.Playing:
- self.mass.event_loop.create_task(poll_task())
class ChromecastProvider(PlayerProvider):
''' support for ChromeCast Audio '''
async def __handle_group_members_update(self, mz, added_player=None, removed_player=None):
''' handle callback from multizone manager '''
if added_player:
- player = await self.get_player(added_player)
group_player = await self.get_player(str(mz._uuid))
- if player and group_player:
- player.group_parent = group_player.player_id
- LOGGER.debug("player %s added to group %s" %(player.name, group_player.name))
+ group_player.add_group_child(added_player)
elif removed_player:
- player = await self.get_player(added_player)
group_player = await self.get_player(str(mz._uuid))
- if player and group_player:
- player.group_parent = None
- LOGGER.debug("player %s removed from group %s" %(player.name, group_player.name))
+ group_player.remove_group_child(added_player)
else:
- for member in mz.members:
- player = await self.get_player(member)
- if player:
- LOGGER.debug("player %s added to group %s" %(player.name, str(mz._uuid)))
- player.group_parent = str(mz._uuid)
+ group_player = await self.get_player(str(mz._uuid))
+ group_player.group_childs = mz.members
@run_periodic(1800)
async def __periodic_chromecast_discovery(self):
for player in self.players:
if not player.cc.socket_client or not player.cc.socket_client.is_connected:
removed_players.append(player.player_id)
- for child_player in player.group_childs:
- # update childs
- child_player.group_parent = None
# cleanup cast object
del player.cc
# signal removed players
player = ChromecastPlayer(self.mass, player_id, self.prov_id)
player.cc = chromecast
player.mz = None
- player.poll_task = False
self.supports_queue = True
self.supports_gapless = False
self.supports_crossfade = False
status_listener = StatusListener(player_id,
player.handle_player_state, self.mass.event_loop)
if chromecast.cast_type == 'group':
- player.is_group = True
mz = MultizoneController(chromecast.uuid)
mz.register_listener(MZListener(mz,
self.__handle_group_members_update, self.mass.event_loop))
chromecast.media_controller.register_status_listener(status_listener)
player.cc.wait()
await self.add_player(player)
- if player.is_group:
+ if player.mz:
player.mz.update_members()
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding:utf-8 -*-
+
+import asyncio
+import aiohttp
+from typing import List
+import logging
+import types
+
+from ..utils import run_periodic, LOGGER, try_parse_int
+from ..models.playerprovider import PlayerProvider
+from ..models.player import Player, PlayerState
+from ..models.playerstate import PlayerState
+from ..models.player_queue import QueueItem, PlayerQueue
+from ..constants import CONF_ENABLED, CONF_HOSTNAME, CONF_PORT
+
+PROV_ID = 'sonos'
+PROV_NAME = 'Sonos'
+PROV_CLASS = 'SonosProvider'
+
+CONFIG_ENTRIES = [
+ (CONF_ENABLED, False, CONF_ENABLED),
+ ]
+
+PLAYER_CONFIG_ENTRIES = []
+
+class SonosPlayer(Player):
+ ''' Sonos player object '''
+
+ async def cmd_stop(self):
+ ''' send stop command to player '''
+ self.soco.stop()
+
+ async def cmd_play(self):
+ ''' send play command to player '''
+ self.soco.play()
+
+ async def cmd_pause(self):
+ ''' send pause command to player '''
+ self.soco.pause()
+
+ async def cmd_next(self):
+ ''' send next track command to player '''
+ self.soco.next()
+
+ async def cmd_previous(self):
+ ''' send previous track command to player '''
+ self.soco.previous()
+
+ async def cmd_power_on(self):
+ ''' send power ON command to player '''
+ self.powered = True
+
+ async def cmd_power_off(self):
+ ''' send power OFF command to player '''
+ self.powered = False
+ # power is not supported so send stop instead
+ self.soco.stop()
+
+ async def cmd_volume_set(self, volume_level):
+ ''' send new volume level command to player '''
+ self.soco.volume = volume_level
+
+ async def cmd_volume_mute(self, is_muted=False):
+ ''' send mute command to player '''
+ self.soco.mute = is_muted
+
+ async def cmd_play_uri(self, uri:str):
+ ''' play single uri on player '''
+ self.soco.play_uri(uri)
+
+ async def cmd_queue_play_index(self, index:int):
+ '''
+ play item at index X on player's queue
+ :attrib index: (int) index of the queue item that should start playing
+ '''
+ self.soco.play_from_queue(index)
+
+ async def cmd_queue_load(self, queue_items:List[QueueItem]):
+ ''' load (overwrite) queue with new items '''
+ self.soco.clear_queue()
+ for pos, item in enumerate(queue_items):
+ self.soco.add_uri_to_queue(item.uri, pos)
+
+ async def cmd_queue_insert(self, queue_items:List[QueueItem], insert_at_index):
+ for pos, item in enumerate(queue_items):
+ self.soco.add_uri_to_queue(item.uri, insert_at_index+pos)
+
+ async def cmd_queue_append(self, queue_items:List[QueueItem]):
+ '''
+ append new items at the end of the queue
+ '''
+ last_index = len(self.queue.items)
+ for pos, item in enumerate(queue_items):
+ self.soco.add_uri_to_queue(item.uri, last_index+pos)
+
+ def _update_state(self, event=None):
+ ''' update state, triggerer by event '''
+ if event:
+ variables = event.variables
+ if "volume" in variables:
+ self.volume_level = int(variables["volume"]["Master"])
+ if "mute" in variables:
+ self.muted = variables["mute"]["Master"] == "1"
+ else:
+ self.volume_level = self.soco.volume
+ self.muted = self.soco.mute
+ transport_info = self.soco.get_current_transport_info()
+ current_transport_state = transport_info.get("current_transport_state")
+ if current_transport_state == "TRANSITIONING":
+ return
+ if self.soco.is_playing_tv or self.soco.is_playing_line_in:
+ self.powered = False
+ else:
+ new_state = self.__convert_state(current_transport_state)
+ self.state = new_state
+ track_info = self.soco.get_current_track_info()
+ self.cur_uri = track_info["uri"]
+ position_info = self.soco.avTransport.GetPositionInfo(
+ [("InstanceID", 0), ("Channel", "Master")])
+ rel_time = self.__timespan_secs(position_info.get("RelTime"))
+ self.cur_time = rel_time
+
+ @staticmethod
+ def __convert_state(sonos_state):
+ ''' convert sonos state to internal state '''
+ if sonos_state == 'PLAYING':
+ return PlayerState.Playing
+ elif sonos_state == 'PAUSED_PLAYBACK':
+ return PlayerState.Paused
+ else:
+ return PlayerState.Stopped
+
+ @staticmethod
+ def __timespan_secs(timespan):
+ """Parse a time-span into number of seconds."""
+ if timespan in ("", "NOT_IMPLEMENTED", None):
+ return None
+ return sum(60 ** x[0] * int(x[1]) for x in enumerate(reversed(timespan.split(":"))))
+
+
+class SonosProvider(PlayerProvider):
+ ''' support for Sonos speakers '''
+
+ def __init__(self, mass, conf):
+ super().__init__(mass, conf)
+ self.prov_id = PROV_ID
+ self.name = PROV_NAME
+ self._discovery_running = False
+ self.player_config_entries = PLAYER_CONFIG_ENTRIES
+
+ async def setup(self):
+ ''' perform async setup '''
+ self.mass.event_loop.create_task(
+ self.__periodic_discovery())
+
+ @run_periodic(1800)
+ async def __periodic_discovery(self):
+ ''' run sonos discovery on interval '''
+ await self.run_discovery()
+
+ async def run_discovery(self):
+ ''' background sonos discovery and handler '''
+ if self._discovery_running:
+ return
+ self._discovery_running = True
+ LOGGER.debug("Sonos discovery started...")
+ import soco
+ discovered_devices = soco.discover()
+ new_device_ids = [item.uid for item in discovered_devices]
+ cur_player_ids = [item.player_id for item in self.players]
+ # remove any disconnected players...
+ for player in self.players:
+ if not player.is_group and not player.soco.uid in new_device_ids:
+ await self.remove_player(player.player_id)
+ # process new players
+ for device in discovered_devices:
+ if device.uid not in cur_player_ids and device.is_visible:
+ await self.__device_discovered(device)
+ # handle groups
+ if len(discovered_devices) > 0:
+ await self.__process_groups(discovered_devices[0].all_groups)
+ else:
+ await self.__process_groups([])
+
+ async def __device_discovered(self, soco_device):
+ '''handle new player '''
+ player = SonosPlayer(self.mass, soco_device.uid, self.prov_id)
+ player.soco = soco_device
+ player.name = soco_device.player_name
+ self.supports_queue = True
+ self.supports_gapless = True
+ self.supports_crossfade = True
+ player._subscriptions = []
+ player._media_position_updated_at = None
+ # handle subscriptions to events
+ def subscribe(service, action):
+ queue = _ProcessSonosEventQueue(action)
+ sub = service.subscribe(auto_renew=True, event_queue=queue)
+ player._subscriptions.append(sub)
+ subscribe(soco_device.avTransport, player._update_state)
+ subscribe(soco_device.renderingControl, player._update_state)
+ subscribe(soco_device.zoneGroupTopology, self.__topology_changed)
+ return await self.add_player(player)
+
+ async def __process_groups(self, sonos_groups):
+ ''' process all sonos groups '''
+ all_group_ids = []
+ for group in sonos_groups:
+ all_group_ids.append(group.uid)
+ if group.uid not in self.mass.players._players:
+ # new group player
+ group_player = await self.__device_discovered(group.coordinator)
+ else:
+ group_player = await self.get_player(group.uid)
+ # check members
+ group_player.name = group.label
+ group_player.group_childs = [item.uid for item in group.members]
+
+ def __topology_changed(self, event=None):
+ '''
+ received topology changed event
+ from one of the sonos players
+ schedule discovery to work out the changes
+ '''
+ self.mass.event_loop.create_task(self.run_discovery())
+
+class _ProcessSonosEventQueue:
+ """Queue like object for dispatching sonos events."""
+
+ def __init__(self, handler):
+ """Initialize Sonos event queue."""
+ self._handler = handler
+
+ def put(self, item, block=True, timeout=None):
+ """Process event."""
+ try:
+ self._handler(item)
+ except Exception as ex:
+ LOGGER.warning("Error calling %s: %s", self._handler, ex)
\ No newline at end of file
if player:
if player._heartbeat_task:
player._heartbeat_task.cancel()
- await self.mass.players.remove_player(player)
+ await self.mass.players.remove_player(player.player_id)
class PySqueezePlayer(Player):
''' Squeezebox socket client '''
total_size_gb = total_size/float(1<<30)
return total_size_gb
-
-def json_serializer(obj):
- ''' json serializer to recursively create serializable values for custom data types '''
+def serialize_values(obj):
+ ''' recursively create serializable values for custom data types '''
def get_val(val):
if isinstance(val, (int, str, bool, float)):
return val
for key, value in val.__dict__.items():
new_dict[key] = get_val(value)
return new_dict
- obj = get_val(obj)
- return json.dumps(obj, skipkeys=True)
+ return get_val(obj)
+
+
+def json_serializer(obj):
+ ''' json serializer to recursively create serializable values for custom data types '''
+ return json.dumps(serialize_values(obj), skipkeys=True)
def try_load_json_file(jsonfile):
else:
return None
except Exception as exc:
- LOGGER.exception("Error loading module %s: %s" %(module_name, exc))
\ No newline at end of file
+ LOGGER.exception("Error loading module %s: %s" %(module_name, exc))
app.add_routes([web.get('/api/players', self.players)])
app.add_routes([web.get('/api/players/{player_id}', self.player)])
app.add_routes([web.get('/api/players/{player_id}/queue', self.player_queue)])
+ app.add_routes([web.get('/api/players/{player_id}/queue/{item_id}', self.player_queue_item)])
app.add_routes([web.get('/api/players/{player_id}/cmd/{cmd}', self.player_command)])
app.add_routes([web.get('/api/players/{player_id}/cmd/{cmd}/{cmd_args}', self.player_command)])
app.add_routes([web.get('/api/players/{player_id}/play_media/{media_type}/{media_id}', self.play_media)])
limit = int(request.query.get('limit', 50))
offset = int(request.query.get('offset', 0))
player = await self.mass.players.get_player(player_id)
- # queue_items = player.queue.items
- # queue_items = [item.__dict__ for item in queue_items]
- # print(queue_items)
- # result = queue_items[offset:limit]
return web.json_response(player.queue.items[offset:limit], dumps=json_serializer)
+
+ async def player_queue_item(self, request):
+ ''' return item (by index or queue item id) from the player's queue '''
+ player_id = request.match_info.get('player_id')
+ item_id = request.match_info.get('item_id')
+ player = await self.mass.players.get_player(player_id)
+ try:
+ item_id = int(item_id)
+ queue_item = await player.queue.get_item(item_id)
+ except:
+ queue_item = await player.queue.by_item_id(item_id)
+ return web.json_response(queue_item, dumps=json_serializer)
async def index(self, request):
index_file = os.path.join(
async def send_event(msg, msg_details):
ws_msg = {"message": msg, "message_details": msg_details }
try:
- await ws.send_json(ws_msg, dumps=json_serializer)
+ await ws.send_json(ws_msg)
except (AssertionError, asyncio.CancelledError):
await self.mass.remove_event_listener(cb_id)
cb_id = await self.mass.add_event_listener(send_event)
<!-- now playing media -->
<v-list-tile avatar ripple>
- <v-list-tile-avatar v-if="active_player.cur_item" style="align-items:center;padding-top:15px;">
- <img v-if="active_player.cur_item.metadata && active_player.cur_item.metadata.image" :src="active_player.cur_item.metadata.image"/>
- <img v-if="!active_player.cur_item.metadata.image && active_player.cur_item.album && active_player.cur_item.album.metadata && active_player.cur_item.album.metadata.image" :src="active_player.cur_item.album.metadata.image"/>
+ <v-list-tile-avatar v-if="cur_player_item" style="align-items:center;padding-top:15px;">
+ <img v-if="cur_player_item && cur_player_item.metadata && cur_player_item.metadata.image" :src="cur_player_item.metadata.image"/>
+ <img v-if="cur_player_item && !cur_player_item.metadata.image && cur_player_item.album && cur_player_item.album.metadata && cur_player_item.album.metadata.image" :src="cur_player_item.album.metadata.image"/>
</v-list-tile-avatar>
<v-list-tile-content style="align-items:center;padding-top:15px;">
- <v-list-tile-title class="title">{{ active_player.cur_item ? active_player.cur_item.name : active_player.name }}</v-list-tile-title>
- <v-list-tile-sub-title v-if="active_player.cur_item && active_player.cur_item.artists">
- <span v-for="(artist, artistindex) in active_player.cur_item.artists">
+ <v-list-tile-title class="title">{{ cur_player_item ? cur_player_item.name : active_player.name }}</v-list-tile-title>
+ <v-list-tile-sub-title v-if="cur_player_item && cur_player_item.artists">
+ <span v-for="(artist, artistindex) in cur_player_item.artists">
<a v-on:click="clickItem(artist)" @click.stop="">{{ artist.name }}</a>
- <label v-if="artistindex + 1 < active_player.cur_item.artists.length" :key="artistindex"> / </label>
+ <label v-if="artistindex + 1 < cur_player_item.artists.length" :key="artistindex"> / </label>
</span>
</v-list-tile-sub-title>
</v-list-tile-content>
<!-- progress bar -->
<div style="color:rgba(0,0,0,.65); height:30px;width:100%; vertical-align: middle; left:15px; right:0; margin-bottom:5px; margin-top:5px">
- <v-layout row style="vertical-align: middle" v-if="active_player.cur_item">
+ <v-layout row style="vertical-align: middle" v-if="cur_player_item">
<span style="text-align:left; width:60px; margin-top:7px; margin-left:15px;">{{ player_time_str_cur }}</span>
<v-progress-linear v-model="progress"></v-progress-linear>
<span style="text-align:right; width:60px; margin-top:7px; margin-right: 15px;">{{ player_time_str_total }}</span>
</v-card-title>
<v-list two-line>
<v-divider></v-divider>
- <div v-for="(player, player_id, index) in players" :key="player_id" v-if="player.enabled && !player.group_parent">
+ <div v-for="(player, player_id, index) in players" :key="player_id" v-if="player.enabled && player.group_parents.length == 0">
<v-list-tile avatar ripple style="margin-left: -5px; margin-right: -15px" @click="switchPlayer(player.player_id)" :style="active_player_id == player.player_id ? 'background-color: rgba(50, 115, 220, 0.3);' : ''">
<v-list-tile-avatar>
- <v-icon size="45">{{ isGroup(player.player_id) ? 'speaker_group' : 'speaker' }}</v-icon>
+ <v-icon size="45">{{ player.is_group ? 'speaker_group' : 'speaker' }}</v-icon>
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title class="title">{{ player.name }}</v-list-tile-title>
- <v-list-tile-sub-title v-if="player.cur_item" class="body-1" :key="player.state">
+ <v-list-tile-sub-title v-if="cur_player_item" class="body-1" :key="player.state">
{{ $t('state.' + player.state) }}
</v-list-tile-sub-title>
$_veeValidate: {
validator: "new"
},
- watch: {},
+ watch: {
+ cur_queue_item: function (val) {
+ // get info for current track
+ if (!val)
+ this.cur_player_item = null;
+ else {
+ const api_url = '/api/players/' + this.active_player_id + '/queue/' + val;
+ axios
+ .get(api_url)
+ .then(result => {
+ if (result.data)
+ this.cur_player_item = result.data;
+ })
+ .catch(error => {
+ console.log("error", error);
+ });
+ }
+ }
+ },
data() {
return {
menu: false,
file: "",
audioPlayer: null,
audioPlayerId: '',
- audioPlayerName: ''
+ audioPlayerName: '',
+ cur_player_item: null
}
},
mounted() {
this.connectWS();
},
computed: {
-
+ cur_queue_item() {
+ if (this.active_player)
+ return this.active_player.cur_queue_item;
+ else
+ return null;
+ },
active_player() {
if (this.players && this.active_player_id && this.active_player_id in this.players)
return this.players[this.active_player_id];
};
},
progress() {
- if (!this.active_player.cur_item)
+ if (!this.cur_player_item)
return 0;
- var total_sec = this.active_player.cur_item.duration;
+ var total_sec = this.cur_player_item.duration;
var cur_sec = this.active_player.cur_time;
var cur_percent = cur_sec/total_sec*100;
return cur_percent;
},
player_time_str_cur() {
- if (!this.active_player.cur_item || !this.active_player.cur_time)
+ if (!this.cur_player_item || !this.active_player.cur_time)
return "0:00";
var cur_sec = this.active_player.cur_time;
return cur_sec.toString().formatDuration();
},
player_time_str_total() {
- if (!this.active_player.cur_item)
+ if (!this.cur_player_item)
return "0:00";
- var total_sec = this.active_player.cur_item.duration;
+ var total_sec = this.cur_player_item.duration;
return total_sec.toString().formatDuration();
}
},
switchPlayer (new_player_id) {
this.active_player_id = new_player_id;
},
- isGroup(player_id) {
- for (var item in this.players)
- if (this.players[item].group_parent == player_id && this.players[item].enabled)
- return true;
- return false;
- },
setPlayerVolume: function(player_id, new_volume) {
this.players[player_id].volume_level = new_volume;
if (new_volume == 'up')
// TODO: store previous player in local storage
if (!this.active_player_id || !this.players[this.active_player_id].enabled)
for (var player_id in this.players)
- if (this.players[player_id].state == 'playing' && this.players[player_id].enabled && !this.players[player_id].group_parent) {
+ if (this.players[player_id].state == 'playing' && this.players[player_id].enabled) {
// prefer the first playing player
this.active_player_id = player_id;
break;
if (!this.active_player_id || !this.players[this.active_player_id].enabled)
for (var player_id in this.players) {
// fallback to just the first player
- if (this.players[player_id].enabled && !this.players[player_id].group_parent)
+ if (this.players[player_id].enabled)
{
this.active_player_id = player_id;
break;
<v-list>\r
<v-list-tile avatar>\r
<v-list-tile-avatar>\r
- <v-icon large>{{ isGroup ? 'speaker_group' : 'speaker' }}</v-icon>\r
+ <v-icon large>{{ players[player_id].is_group ? 'speaker_group' : 'speaker' }}</v-icon>\r
</v-list-tile-avatar>\r
<v-list-tile-content>\r
<v-list-tile-title>{{ players[player_id].name }}</v-list-tile-title>\r
},\r
computed: {\r
volumePlayerIds() {\r
- var volume_ids = [this.player_id];\r
- for (var player_id in this.players)\r
- if (this.players[player_id].group_parent == this.player_id && this.players[player_id].enabled)\r
- volume_ids.push(player_id);\r
- return volume_ids;\r
- },\r
- isGroup() {\r
- return this.volumePlayerIds.length > 1;\r
+ var all_ids = [this.player_id];\r
+ all_ids.push(...this.players[this.player_id].group_childs);\r
+ return all_ids;\r
}\r
},\r
mounted() { },\r
aiohttp
pyloudnorm
SoundFile
-aiorun
\ No newline at end of file
+aiorun
+soco
\ No newline at end of file