various small improvements
authormarcelveldt <marcelvanderveldt@MacBook-Pro.local>
Wed, 15 May 2019 18:57:58 +0000 (20:57 +0200)
committermarcelveldt <marcelvanderveldt@MacBook-Pro.local>
Wed, 15 May 2019 18:57:58 +0000 (20:57 +0200)
bugfixes

config.json
music_assistant/database.py
music_assistant/main.py
music_assistant/models.py
music_assistant/modules/homeassistant.py
music_assistant/modules/musicproviders/spotify.py
music_assistant/modules/playerproviders/lms.py
music_assistant/music.py
music_assistant/player.py
music_assistant/web/components/headermenu.vue.js
music_assistant/web/pages/home.vue.js

index 4868e218062ec012c8c9a71ec6a73d0300d9fde9..16ba77aca812eddea29f1a8eb82ab29b0e86920d 100755 (executable)
@@ -1,6 +1,6 @@
 {
   "name": "Music Assistant",
-  "version": "0.0.4",
+  "version": "0.0.5",
   "description": "Media library manager for (streaming) media",
   "slug": "music_assistant",
   "startup": "application",
index 7fc884cdbafe03e5f3ee2a801935e05f84274f5c..3c856ce5e30444e819b58ae00927bcea26388d1d 100755 (executable)
@@ -178,6 +178,9 @@ class Database():
             if media_type == MediaType.Playlist:
                 sql_query = 'DELETE FROM playlist_tracks WHERE playlist_id=?;'
                 await db.execute(sql_query, (item_id,))
+            if media_type == MediaType.Playlist:
+                sql_query = 'DELETE FROM playlists WHERE playlist_id=?;'
+                await db.execute(sql_query, (item_id,))
             await db.commit()
     
     async def artists(self, filter_query=None, limit=100000, offset=0, orderby='name', fulldata=False) -> List[Artist]:
index 6f66ef49b225fd8edef73d6cd4095156b1e95cff..a3805a89d901f0270a9175dbb28f8a98eaf3d6ca 100755 (executable)
@@ -4,6 +4,7 @@
 import sys
 import asyncio
 from concurrent.futures import ThreadPoolExecutor
+from contextlib import suppress
 import re
 import uvloop
 import os
@@ -32,10 +33,6 @@ class Main():
         self.bg_executor = ThreadPoolExecutor(max_workers=5)
         self.event_listeners = {}
 
-        import signal
-        signal.signal(signal.SIGINT, self.stop)
-        signal.signal(signal.SIGTERM, self.stop)
-
         # init database and metadata modules
         self.db = Database(datapath, self.event_loop)
         # allow some time for the database to initialize
@@ -51,7 +48,14 @@ class Main():
         self.player = Player(self)
 
         # start the event loop
-        self.event_loop.run_forever()
+        try:
+            self.event_loop.run_forever()
+        except (KeyboardInterrupt, SystemExit):
+            LOGGER.info('Exit requested!')
+            self.save_config()
+            self.event_loop.close()
+            LOGGER.info('Shutdown complete.')
+
 
     async def event(self, msg, msg_details=None):
         ''' signal event '''
@@ -85,38 +89,16 @@ class Main():
             "base": {},
             "musicproviders": {},
             "playerproviders": {},
-            "player_settings": 
-                {
-                    "__desc__":
-                    [
-                        ("enabled", False, "Enable player"),
-                        ("name", "", "Custom name for this player"),
-                        ("group_parent", "<player>", "Group this player with another player"),
-                        ("mute_as_power", False, "Use muting as power control"),
-                        ("disable_volume", False, "Disable volume controls"),
-                        ("apply_group_volume", False, "Apply group volume to childs (for group players only)")
-                    ]
-                }
+            "player_settings": {}
             }
         conf_file = os.path.join(self._datapath, 'config.json')
         if os.path.isfile(conf_file):
             with open(conf_file) as f:
                 data = f.read()
-                stored_config = json.loads(data)
-                for key in config.keys():
-                    if stored_config.get(key):
-                        config[key].update(stored_config[key])
+                if data:
+                    config = json.loads(data)
         self.config = config
 
-    def stop(self, signum=None, frame=None):
-        ''' properly close all connections'''
-        print('stop requested!')
-        self.save_config()
-        self.web.stop()
-        print('stopping event loop...')
-        self.event_loop.stop()
-        self.event_loop.close()
-
 if __name__ == "__main__":
     datapath = sys.argv[1]
     if not datapath:
index 421ccbe1b5a9751321677c94905db41b51bab4c0..be3044b4ecedd6c3f8cf869a3184aec090b5123b 100755 (executable)
@@ -460,9 +460,7 @@ class MusicPlayer():
         self.muted = False
         self.group_parent = None # set to id of REAL group/parent player
         self.is_group = False # is this player a group player ?
-        self.disable_volume = False
-        self.mute_as_power = False
-        self.apply_group_volume = False
+        self.settings = {}
         self.enabled = False
 
 class PlayerProvider():
index 699906b7929ccc17a0477889d33687a9c38a7930..7cb9dc97a442008f35a2126373aabb62d6d27b44 100644 (file)
@@ -74,6 +74,7 @@ class HomeAssistant():
         self._published_players = {}
         self._tracked_states = {}
         self._state_listeners = []
+        self._sources = []
         self._token = token
         if url.startswith('https://'):
             self._use_ssl = True
@@ -87,6 +88,7 @@ class HomeAssistant():
         LOGGER.info('Homeassistant integration is enabled')
         mass.event_loop.create_task(self.__hass_websocket())
         mass.event_loop.create_task(self.mass.add_event_listener(self.mass_event))
+        mass.event_loop.create_task(self.__get_sources())
 
     async def get_state(self, entity_id, attribute='state', register_listener=None):
         ''' get state of a hass entity'''
@@ -159,8 +161,32 @@ class HomeAssistant():
                     await self.mass.player.player_command(player_id, 'next')
                 elif service == 'media_play_pause':
                     await self.mass.player.player_command(player_id, 'pause', 'toggle')
-                # TODO: handle media play !
+                elif service == 'play_media':
+                    return await self.__handle_play_media(player_id, service_data)
 
+    async def __handle_play_media(self, player_id, service_data):
+        ''' handle play_media request from homeassistant'''
+        media_content_type = service_data['media_content_type'].lower()
+        media_content_id = service_data['media_content_id']
+        queue_opt = 'add' if service_data.get('enqueue') else 'play'
+        if media_content_type == 'playlist' and not '://' in media_content_id:
+            media_items = []
+            for playlist_str in media_content_id.split(','):
+                playlist_str = playlist_str.strip()
+                playlist = await self.mass.music.playlist_by_name(playlist_str)
+                if playlist:
+                    media_items.append(playlist)
+            return await self.mass.player.play_media(player_id, media_items, queue_opt)
+        elif media_content_type == 'playlist' and 'spotify://playlist' in media_content_id:
+            # TODO: handle parsing of other uri's here
+            playlist = self.mass.music.providers['spotify'].playlist(media_content_id.split(':')[-1])
+            return await self.mass.player.play_media(player_id, playlist, queue_opt)
+        elif media_content_id.startswith('http'):
+            track = Track()
+            track.uri = media_content_id
+            track.provider = 'http'
+            return await self.mass.player.play_media(player_id, track, queue_opt)
+    
     async def publish_player(self, player):
         ''' publish player details to hass'''
         if not self.mass.config['base']['homeassistant']['publish_players']:
@@ -169,8 +195,10 @@ class HomeAssistant():
         entity_id = 'media_player.mass_' + slug.slugify(player.name, separator='_').lower()
         state = player.state if player.powered else 'off'
         state_attributes = {
-                "supported_features": 58303
+                "supported_features": 65471
                 "friendly_name": player.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,
@@ -196,6 +224,11 @@ class HomeAssistant():
             msg['service_data'] = service_data
         return await self.__send_ws(msg)
 
+    @run_periodic(120)
+    async def __get_sources(self):
+        ''' we build a list of all playlists to use as player sources '''
+        self._sources = [playlist.name for playlist in await self.mass.music.playlists()]
+
     async def __set_state(self, entity_id, new_state, state_attributes={}):
         ''' set state to hass entity '''
         data = {
@@ -207,7 +240,7 @@ class HomeAssistant():
     
     async def __hass_websocket(self):
         ''' Receive events from Hass through websockets '''
-        while True:
+        while self.mass.event_loop.is_running():
             try:
                 protocol = 'wss' if self._use_ssl else 'ws'
                 async with self.http_session.ws_connect('%s://%s/api/websocket' % (protocol, self._host)) as ws:
@@ -250,7 +283,7 @@ class HomeAssistant():
                             break
             except Exception as exc:
                 LOGGER.exception(exc)
-                asyncio.sleep(10)
+                await asyncio.sleep(10)
 
     async def __get_data(self, endpoint):
         ''' get data from hass rest api'''
index cae6aae1fa7646a70c82528f4959a80da8aab6f9..6725943af9ede2480d3a0330064022f6119d65ff 100644 (file)
@@ -255,6 +255,7 @@ class SpotifyProvider(MusicProvider):
             'bit_depth': 16,
             'url': 'http://%s/stream/spotify/%s' % (host, track_id)
         }
+    
     async def get_stream(self, track_id):
         ''' get audio stream for a track '''
         sox_effects='vol -12 dB'
index 85d26a2121d79d4fec59bcad146ab9c2de730003..8faf1601dc3de0d2b1f06917a008b5f99561565a 100644 (file)
@@ -116,13 +116,11 @@ class LMSProvider(PlayerProvider):
     async def player_queue(self, player_id, offset=0, limit=50):
         ''' return the items in the player's queue '''
         items = []
-        cur_index = await self.__get_data(["playlist", "index", "?"], player_id=player_id)
-        cur_index = int(cur_index['_index'])
-        offset += cur_index # we do not care about already played tracks
         player_details = await self.__get_data(["status", offset, limit, "tags:aAcCdegGijJKlostuxyRwk"], player_id=player_id)
-        for item in player_details['playlist_loop']:
-            track = await self.__parse_track(item)
-            items.append(track)
+        if 'playlist_loop' in player_details:
+            for item in player_details['playlist_loop']:
+                track = await self.__parse_track(item)
+                items.append(track)
         return items
 
     ### Provider specific (helper) methods #####
@@ -223,8 +221,9 @@ class LMSProvider(PlayerProvider):
         track = Track()
         track.name = track_details['title']
         track.duration = int(track_details['duration'])
-        image = "http://%s:%s%s" % (self._host, self._port, track_details['artwork_url'])
-        track.metadata['image'] = image
+        if 'artwork_url' in track_details:
+            image = "http://%s:%s%s" % (self._host, self._port, track_details['artwork_url'])
+            track.metadata['image'] = image
         return track
 
     async def __get_group_childs(self, group_player_id):
@@ -237,7 +236,7 @@ class LMSProvider(PlayerProvider):
     
     async def __lms_events(self):
         # Receive events from LMS through CometD socket
-        while True:
+        while self.mass.event_loop.is_running():
             try:
                 last_msg_received = 0
                 async with Client("http://%s:%s/cometd" % (self._host, self._port), 
index 4d3753edfd1b8556f462198f7eb05121670852a0..30264438ee8e946940168a871ad53161f5743ae8 100755 (executable)
@@ -91,6 +91,13 @@ class Music():
             return await self.mass.db.playlist(item_id)
         return await self.providers[provider].playlist(item_id)
 
+    async def playlist_by_name(self, name):
+        ''' get playlist by name '''
+        for playlist in await self.playlists():
+            if playlist.name == name:
+                return playlist
+        return None
+    
     async def artist_toptracks(self, artist_id, provider='database'):
         ''' get top tracks for given artist '''
         artist = await self.artist(artist_id, provider)
index 8a295c19b904018b5f36d3e1a2f42b94c18ceac2..a3d341bcdd962b326528506ce0d83cbec3978234 100755 (executable)
@@ -25,9 +25,23 @@ class Player():
         self.mass = mass
         self.providers = {}
         self._players = {}
+        self.create_config_entries()
         # dynamically load provider modules
         self.load_providers()
 
+    def create_config_entries(self):
+        ''' sets the config entries for this module (list with key/value pairs)'''
+        self.mass.config['player_settings']['__desc__'] = [
+            ("enabled", False, "Enable player"),
+            ("name", "", "Custom name for this player"),
+            ("group_parent", "<player>", "Group this player to another player"),
+            ("mute_as_power", False, "Use muting as power control"),
+            ("disable_volume", False, "Disable volume controls"),
+            ("apply_group_volume", False, "Apply group volume to childs (for group players only)"),
+            ("apply_group_power", False, "Apply group power based on childs (for group players only)"),
+            ("play_power_on", False, "Issue play command on power on")
+        ]
+    
     async def players(self):
         ''' return all players '''
         items = list(self._players.values())
@@ -40,8 +54,9 @@ class Player():
 
     async def player_command(self, player_id, cmd, cmd_args=None):
         ''' issue command on player (play, pause, next, previous, stop, power, volume, mute) '''
+        if player_id not in self._players:
+            return
         player = self._players[player_id]
-        player_settings = await self.get_player_config(player)
         # handle some common workarounds
         if cmd in ['pause', 'play'] and cmd_args == 'toggle':
             cmd = 'pause' if player.state == PlayerState.Playing else 'play'
@@ -51,48 +66,62 @@ class Player():
             cmd_args = player.volume_level + 2
         elif cmd == 'volume' and cmd_args == 'down':
             cmd_args = player.volume_level - 2
+        # redirect playlist related commands to parent player
         if player.group_parent and cmd not in ['power', 'volume', 'mute']:
-            # redirect playlist related commands to parent player
             return await self.player_command(player.group_parent, cmd, cmd_args)
         # handle hass integration
-        if self.mass.hass:
-            if cmd == 'power' and cmd_args == 'on' and player_settings.get('hass_power_entity') and player_settings.get('hass_power_entity_source'):
-                service_data = { 'entity_id': player_settings['hass_power_entity'], 'source':player_settings['hass_power_entity_source'] }
-                await self.mass.hass.call_service('media_player', 'select_source', service_data)
-            elif cmd == 'power' and player_settings.get('hass_power_entity'):
-                domain = player_settings['hass_power_entity'].split('.')[0]
-                service_data = { 'entity_id': player_settings['hass_power_entity']}
-                await self.mass.hass.call_service(domain, 'turn_%s' % cmd_args, service_data)
-            if cmd == 'volume' and player_settings.get('hass_volume_entity'):
-                service_data = { 'entity_id': player_settings['hass_power_entity'], 'volume_level': int(cmd_args)/100}
-                await self.mass.hass.call_service('media_player', 'volume_set', service_data)
-                cmd_args = 100 # just force full volume on actual player if volume is outsourced to hass
-        if cmd == 'power' and player.mute_as_power:
+        await self.__player_command_hass_integration(player, cmd, cmd_args)
+        # handle mute as power
+        if cmd == 'power' and player.settings['mute_as_power']:
             cmd = 'mute'
             cmd_args = 'on' if cmd_args == 'off' else 'off' # invert logic (power ON is mute OFF)
+        # handle group volume for group players
         player_childs = [item for item in self._players.values() if item.group_parent == player_id]
-        is_group = len(player_childs) > 0
-        if is_group and cmd == 'volume' and player.apply_group_volume:
-            # group volume, apply to childs (if any)
-            cur_volume = player.volume_level
-            new_volume = try_parse_int(cmd_args)
-            if new_volume < cur_volume:
-                volume_dif = new_volume - cur_volume
-            else:
-                volume_dif = cur_volume - new_volume
-            for child_player in player_childs:
-                if child_player.enabled and child_player.powered:
-                    cur_child_volume = child_player.volume_level
-                    new_child_volume = cur_child_volume + volume_dif
-                    LOGGER.debug('apply group volume %s to child %s' %(new_child_volume, child_player.name))
-                    await self.player_command(child_player.player_id, 'volume', new_child_volume)
-            player.volume_level = new_volume
-            return True
-        else:
-            prov_id = self._players[player_id].player_provider
-            prov = self.providers[prov_id]
-            return await prov.player_command(player_id, cmd, cmd_args)
+        if player.is_group and cmd == 'volume' and player.settings['apply_group_volume']:
+            return await self.__player_command_group_volume(player, player_childs, cmd_args)
+        if player.is_group and cmd == 'power' and cmd_args == 'off':
+            for item in player_childs:
+                asyncio.create_task(self.player_command(item.player_id, cmd, cmd_args))
+        # normal execution of command on player
+        prov_id = self._players[player_id].player_provider
+        prov = self.providers[prov_id]
+        await prov.player_command(player_id, cmd, cmd_args)
+        # handle play on power on
+        if cmd == 'power' and cmd_args == 'on' and player.settings['play_power_on']:
+            LOGGER.info('play_power_on %s' % player.name)
+            await prov.player_command(player_id, 'play')
+
+    async def __player_command_hass_integration(self, player, cmd, cmd_args):
+        ''' handle hass integration in player command '''
+        if not self.mass.hass:
+            return
+        if cmd == 'power' and cmd_args == 'on' and player.settings.get('hass_power_entity') and player.settings.get('hass_power_entity_source'):
+            service_data = { 'entity_id': player.settings['hass_power_entity'], 'source':player.settings['hass_power_entity_source'] }
+            await self.mass.hass.call_service('media_player', 'select_source', service_data)
+        elif cmd == 'power' and player.settings.get('hass_power_entity'):
+            domain = player.settings['hass_power_entity'].split('.')[0]
+            service_data = { 'entity_id': player.settings['hass_power_entity']}
+            await self.mass.hass.call_service(domain, 'turn_%s' % cmd_args, service_data)
+        if cmd == 'volume' and player.settings.get('hass_volume_entity'):
+            service_data = { 'entity_id': player.settings['hass_power_entity'], 'volume_level': int(cmd_args)/100}
+            await self.mass.hass.call_service('media_player', 'volume_set', service_data)
+            cmd_args = 100 # just force full volume on actual player if volume is outsourced to hass
             
+    async def __player_command_group_volume(self, player, player_childs, cmd_args):
+        ''' handle group volume if needed'''
+        cur_volume = player.volume_level
+        new_volume = try_parse_int(cmd_args)
+        volume_dif = new_volume - cur_volume
+        volume_dif_percent = volume_dif/cur_volume
+        for child_player in player_childs:
+            if 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)
+                child_player.volume_level = new_child_volume
+                await self.player_command(child_player.player_id, 'volume', new_child_volume)
+        player.volume_level = new_volume
+        return True
+
     async def remove_player(self, player_id):
         ''' handle a player remove '''
         self._players.pop(player_id, None)
@@ -107,7 +136,6 @@ class Player():
         player_details = deepcopy(player_details)
         LOGGER.debug('Incoming msg from %s' % player_details.name)
         player_id = player_details.player_id
-        player_settings = await self.get_player_config(player_details)
         player_changed = False
         if not player_id in self._players:
             # first message from player
@@ -118,128 +146,154 @@ class Player():
             player_changed = True
         else:
             player = self._players[player_id]
-        
+        player.settings = await self.__get_player_settings(player_id)
         # handle basic player settings
-        player_details.enabled = player_settings['enabled']
-        player_details.name = player_settings['name']
-        player_details.disable_volume = player_settings['disable_volume']
-        player_details.mute_as_power = player_settings['mute_as_power']
-        player_details.apply_group_volume = player_settings['apply_group_volume']
-
+        player_details.enabled = player.settings['enabled']
+        player_details.name = player.settings['name'] if player.settings['name'] else player_details.name
+        player_details.group_parent = player.settings['group_parent'] if player.settings['group_parent'] else player_details.group_parent
         # handle hass integration
-        if self.mass.hass:
-            if player_settings.get('hass_power_entity') and player_settings.get('hass_power_entity_source'):
-                hass_state = await self.mass.hass.get_state(
-                        player_settings['hass_power_entity'],
-                        attribute='source',
-                        register_listener=functools.partial(self.trigger_update, player_id))
-                player_details.powered = hass_state == player_settings['hass_power_entity_source']
-            elif player_settings.get('hass_power_entity'):
-                hass_state = await self.mass.hass.get_state(
-                        player_settings['hass_power_entity'],
-                        attribute='state',
-                        register_listener=functools.partial(self.trigger_update, player_id))
-                player_details.powered = hass_state != 'off'
-            if player_settings.get('hass_volume_entity'):
-                hass_state = await self.mass.hass.get_state(
-                        player_settings['hass_volume_entity'], 
-                        attribute='volume_level',
-                        register_listener=functools.partial(self.trigger_update, player_id))
-                player_details.volume_level = int(try_parse_float(hass_state)*100)
-        
+        await self.__update_player_hass_settings(player_details, player.settings)
         # handle mute as power setting
-        if player_details.mute_as_power:
+        if player.settings['mute_as_power']:
             player_details.powered = not player_details.muted
         # combine state of group parent
-        if player_settings['group_parent']:
-            player_details.group_parent = player_settings['group_parent']
         if player_details.group_parent and player_details.group_parent in self._players:
             parent_player = self._players[player_details.group_parent]
             player_details.cur_item_time = parent_player.cur_item_time
             player_details.cur_item = parent_player.cur_item
             player_details.state = parent_player.state
-        # handle group volume setting
+        # handle group volume/power setting
         player_childs = [item for item in self._players.values() if item.group_parent == player_id]
         player_details.is_group = len(player_childs) > 0
-        if player_details.is_group and player_details.apply_group_volume:
-            group_volume = 0
-            active_players = 0
-            for child_player in player_childs:
-                if child_player.enabled and child_player.powered:
-                    group_volume += child_player.volume_level
-                    active_players += 1
-            group_volume = group_volume / active_players if active_players else 0
-            player_details.volume_level = group_volume
+        if player_details.is_group and player.settings['apply_group_volume']:
+            await self.__update_player_group_volume(player_details, player_childs)
+        if player_details.is_group and player.settings['apply_group_power']:
+            await self.__update_player_group_power(player_details, player_childs)
         # compare values to detect changes
         for key, cur_value in player.__dict__.items():
-            new_value = getattr(player_details, key)
-            if new_value != cur_value:
-                player_changed = True
-                setattr(player, key, new_value)
-                LOGGER.debug('key changed: %s for player %s - new value: %s' % (key, player.name, new_value))
+            if key != 'settings':
+                new_value = getattr(player_details, key)
+                if new_value != cur_value:
+                    player_changed = True
+                    setattr(player, key, new_value)
+                    LOGGER.debug('key changed: %s for player %s - new value: %s' % (key, player.name, new_value))
         if player_changed:
             # player is added or updated!
             asyncio.ensure_future(self.mass.event('player updated', player))
+            # is groupplayer, trigger update of its childs
             for child in player_childs:
                 asyncio.create_task(self.trigger_update(child.player_id))
+            # if child player in a group, trigger update of parent
+            if player.group_parent:
+                asyncio.create_task(self.trigger_update(player.group_parent))
 
-    async def get_player_config(self, player_details):
-        ''' get or create player config '''
+    async def __update_player_hass_settings(self, player_details, player_settings):
+        ''' handle home assistant integration on a player '''
+        if not self.mass.hass:
+            return
         player_id = player_details.player_id
-        if player_id in self.mass.config['player_settings']:
-            return self.mass.config['player_settings'][player_id]
-        new_config = {
-                "name": player_details.name,
-                "group_parent": player_details.group_parent,
-                "mute_as_power": False,
-                "disable_volume": False,
-                "apply_group_volume": False,
-                "enabled": False
-            }
-        self.mass.config['player_settings'][player_id] = new_config
-        return new_config
+        player_settings = self.mass.config['player_settings'][player_id]
+        if player_settings.get('hass_power_entity') and player_settings.get('hass_power_entity_source'):
+            hass_state = await self.mass.hass.get_state(
+                    player_settings['hass_power_entity'],
+                    attribute='source',
+                    register_listener=functools.partial(self.trigger_update, player_id))
+            player_details.powered = hass_state == player_settings['hass_power_entity_source']
+        elif player_settings.get('hass_power_entity'):
+            hass_state = await self.mass.hass.get_state(
+                    player_settings['hass_power_entity'],
+                    attribute='state',
+                    register_listener=functools.partial(self.trigger_update, player_id))
+            player_details.powered = hass_state != 'off'
+        if player_settings.get('hass_volume_entity'):
+            hass_state = await self.mass.hass.get_state(
+                    player_settings['hass_volume_entity'], 
+                    attribute='volume_level',
+                    register_listener=functools.partial(self.trigger_update, player_id))
+            player_details.volume_level = int(try_parse_float(hass_state)*100)
+    
+    async def __update_player_group_volume(self, player_details, player_childs):
+        ''' handle group volume '''
+        group_volume = 0
+        active_players = 0
+        for child_player in player_childs:
+            if child_player.enabled and child_player.powered:
+                group_volume += child_player.volume_level
+                active_players += 1
+        group_volume = group_volume / active_players if active_players else 0
+        player_details.volume_level = group_volume
+    
+    async def __update_player_group_power(self, player_details, player_childs):
+        ''' handle group power '''
+        player_powered = False
+        for child_player in player_childs:
+            if child_player.powered:
+                player_powered = True
+                break
+        if player_details.powered and not player_powered:
+            # all childs turned off so turn off group player
+            LOGGER.info('all childs turned off so turn off group player %s' % player_details.name)
+            await self. player_command(player_details.player_id, 'power', 'off')
+            player_details.powered = False
+        elif not player_details.powered and player_powered:
+            # all childs turned off but group player still off, so turn it on
+            LOGGER.info('all childs turned off but group player still off, so turn it on %s' % player_details.name)
+            await self. player_command(player_details.player_id, 'power', 'on')
+            player_details.powered = True
+
+    async def __get_player_settings(self, player_id):
+        ''' get (or create) player config '''
+        player_settings = self.mass.config['player_settings'].get(player_id,{})
+        for key, def_value, desc in self.mass.config['player_settings']['__desc__']:
+            if not key in player_settings:
+                player_settings[key] = def_value
+        self.mass.config['player_settings'][player_id] = player_settings
+        return player_settings
 
-    async def play_media(self, player_id, media_item, queue_opt='replace'):
+    async def play_media(self, player_id, media_item, queue_opt='play'):
         ''' 
             play media on a player 
             player_id: id of the player
-            media_item: media item that should be played (Track, Album, Artist, Playlist)
+            media_item: media item(s) that should be played (Track, Album, Artist, Playlist)
             queue_opt: play, replace, next or add
         '''
         if not player_id in self._players:
             LOGGER.warning('Player %s not found' % player_id)
             return False
         player_prov = self.providers[self._players[player_id].player_provider]
-        # collect tracks to play
-        if media_item.media_type == MediaType.Artist:
-            tracks = await self.mass.music.artist_toptracks(media_item.item_id, provider=media_item.provider)
-        elif media_item.media_type == MediaType.Album:
-            tracks = await self.mass.music.album_tracks(media_item.item_id, provider=media_item.provider)
-        elif media_item.media_type == MediaType.Playlist:
-            tracks = await self.mass.music.playlist_tracks(media_item.item_id, provider=media_item.provider, offset=0, limit=0) 
-        else:
-            tracks = [media_item] # single track
-        # check supported music providers by this player and work out how to handle playback...
+        # a single item or list of items may be provided
+        media_items = media_item if isinstance(media_item, list) else [media_item]
         playable_tracks = []
-        for track in tracks:
-            # sort by quality
-            match_found = False
-            for prov_media in sorted(track.provider_ids, key=operator.itemgetter('quality'), reverse=True):
-                media_provider = prov_media['provider']
-                media_item_id = prov_media['item_id']
-                player_supported_provs = player_prov.supported_musicproviders
-                if media_provider in player_supported_provs:
-                    # the provider can handle this media_type directly !
-                    track.uri = await self.get_track_uri(media_item_id, media_provider)
-                    playable_tracks.append(track)
-                    match_found = True
-                elif 'http' in player_prov.supported_musicproviders:
-                    # fallback to http streaming if supported
-                    track.uri = await self.get_track_uri(media_item_id, media_provider, True)
-                    playable_tracks.append(track)
-                    match_found = True
-                if match_found:
-                    break
+        for media_item in media_items:
+            # collect tracks to play
+            if media_item.media_type == MediaType.Artist:
+                tracks = await self.mass.music.artist_toptracks(media_item.item_id, provider=media_item.provider)
+            elif media_item.media_type == MediaType.Album:
+                tracks = await self.mass.music.album_tracks(media_item.item_id, provider=media_item.provider)
+            elif media_item.media_type == MediaType.Playlist:
+                tracks = await self.mass.music.playlist_tracks(media_item.item_id, provider=media_item.provider, offset=0, limit=0) 
+            else:
+                tracks = [media_item] # single track
+            # check supported music providers by this player and work out how to handle playback...
+            for track in tracks:
+                # sort by quality
+                match_found = False
+                for prov_media in sorted(track.provider_ids, key=operator.itemgetter('quality'), reverse=True):
+                    media_provider = prov_media['provider']
+                    media_item_id = prov_media['item_id']
+                    player_supported_provs = player_prov.supported_musicproviders
+                    if media_provider in player_supported_provs:
+                        # the provider can handle this media_type directly !
+                        track.uri = await self.get_track_uri(media_item_id, media_provider)
+                        playable_tracks.append(track)
+                        match_found = True
+                    elif 'http' in player_prov.supported_musicproviders:
+                        # fallback to http streaming if supported
+                        track.uri = await self.get_track_uri(media_item_id, media_provider, True)
+                        playable_tracks.append(track)
+                        match_found = True
+                    if match_found:
+                        break
         if playable_tracks:
             if self._players[player_id].shuffle_enabled:
                 random.shuffle(playable_tracks)
@@ -247,7 +301,7 @@ class Player():
                 queue_opt = 'replace' # always assume playback of multiple items as new queue
             return await player_prov.play_media(player_id, playable_tracks, queue_opt)
         else:
-            raise Exception("Musicprovider %s and/or mediatype %s not supported by player %s !" % ("/".join(media_item.provider_ids), media_item.media_type, player_id) )
+            raise Exception("Musicprovider and/or media not supported by player %s !" % (player_id) )
     
     async def get_track_uri(self, item_id, provider, http_stream=False):
         ''' generate the URL/URI for a media item '''
index d5505c7522e599d0b7425345af21aee99e0e0904..1fe2815be2be9ca74926d6b06ebd8e23aaab97c1 100755 (executable)
@@ -24,7 +24,7 @@ Vue.component("headermenu", {
             </v-btn>
             <v-spacer></v-spacer>
             <v-spacer></v-spacer>
-            <v-btn icon>
+            <v-btn icon v-on:click="$router.push('/search')">
                 <v-icon>search</v-icon>
               </v-btn>
         </v-layout>
index 348a407d0a7d2e50e0b0870eb941d60228b6e89a..be0010ebfe1a1010290c775c1918f7bd330eb819 100755 (executable)
@@ -1,16 +1,27 @@
 var home = Vue.component("Home", {
   template: `
-  <v-list>
-    <v-list-tile 
-      v-for="item in items" :key="item.title" @click="$router.push(item.path)">
-        <v-list-tile-action style="margin-left:15px">
-            <v-icon>{{ item.icon }}</v-icon>
-        </v-list-tile-action>
-        <v-list-tile-content>
-            <v-list-tile-title>{{ item.title }}</v-list-tile-title>
-        </v-list-tile-content>
-    </v-list-tile>
-  </v-list>
+  <section>
+      <v-flex xs12 justify-center>
+        <v-card color="cyan darken-2" class="white--text" img="../images/info_gradient.jpg">
+        
+          <div class="text-xs-center" style="height:40px" id="whitespace_top"/>      
+          <v-card-title class="display-1 justify-center" style="text-shadow: 1px 1px #000000;">
+              Music Assistant
+          </v-card-title>
+        </v-card>
+      </v-flex>    
+      <v-list>
+        <v-list-tile 
+          v-for="item in items" :key="item.title" @click="$router.push(item.path)">
+            <v-list-tile-action style="margin-left:15px">
+                <v-icon>{{ item.icon }}</v-icon>
+            </v-list-tile-action>
+            <v-list-tile-content>
+                <v-list-tile-title>{{ item.title }}</v-list-tile-title>
+            </v-list-tile-content>
+        </v-list-tile>
+      </v-list>
+  </section>
 `,
   props: ["title"],
   $_veeValidate: {
@@ -25,10 +36,11 @@ var home = Vue.component("Home", {
   created() {
     this.$globals.windowtitle = "Home"
     this.items= [
-      { title: 'Artists', path: '/browse/library/artists', icon: "person" },
-      { title: 'Albums', path: '/browse/library/albums', icon: "album" },
-      { title: 'Tracks', path: '/browse/library/tracks', icon: "audiotrack" },
-      { title: 'Playlists', path: '/browse/library/playlists', icon: "playlist_play" }
+        { title: "Artists", icon: "person", path: "/artists" },
+        { title: "Albums", icon: "album", path: "/albums" },
+        { title: "Tracks", icon: "audiotrack", path: "/tracks" },
+        { title: "Playlists", icon: "playlist_play", path: "/playlists" },
+        { title: "Search", icon: "search", path: "/search" }
     ]
   },
   methods: {