stability fixes
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 28 May 2019 13:48:53 +0000 (15:48 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 28 May 2019 13:48:53 +0000 (15:48 +0200)
music_assistant/modules/http_streamer.py
music_assistant/modules/musicproviders/qobuz.py
music_assistant/modules/musicproviders/spotify.py
music_assistant/modules/player.py
music_assistant/modules/playerproviders/chromecast.py
music_assistant/modules/playerproviders/pylms.py
music_assistant/modules/web.py
music_assistant/utils.py
music_assistant/web/components/player.vue.js

index 751d4aeddc9265779bb737cece55f9e94898c6e0..a1ced4e6eceaa92d6c738623798abe3564399443 100755 (executable)
@@ -9,7 +9,6 @@ import shutil
 import xml.etree.ElementTree as ET
 import random
 
-
 AUDIO_TEMP_DIR = "/tmp/audio_tmp"
 AUDIO_CACHE_DIR = "/tmp/audio_cache"
 
@@ -42,23 +41,7 @@ class HTTPStreamer():
             if not key in self.mass.config['base']['http_streamer']:
                 self.mass.config['base']['http_streamer'][key] = def_value
     
-    async def get_audio_stream(self, track_id, provider, player_id=None):
-        ''' get audio stream for a track '''
-        queue = asyncio.Queue()
-        run_async_background_task(
-            self.mass.bg_executor, self.__get_audio_stream, queue, track_id, provider, player_id)
-        while True:
-            chunk = await queue.get()
-            if not chunk:
-                queue.task_done()
-                break
-            yield chunk
-            queue.task_done()
-        await queue.join()
-        # TODO: handle disconnects ?
-        LOGGER.info("Finished streaming %s" % track_id)
-
-    async def __get_audio_stream(self, audioqueue, track_id, provider, player_id=None):
+    async def get_audio_stream(self, audioqueue, track_id, provider, player_id=None):
         ''' get audio stream from provider and apply additional effects/processing where/if needed'''
         input_content_type = await self.mass.music.providers[provider].get_stream_content_type(track_id)
         cachefile = self.__get_track_cache_filename(track_id, provider)
@@ -86,11 +69,12 @@ class HTTPStreamer():
                      self.__fill_audio_buffer(process.stdin, track_id, provider, input_content_type))
         # put chunks from stdout into queue
         while not process.stdout.at_eof():
-            chunk = await process.stdout.read(10240000)
+            chunk = await process.stdout.read(256000)
             if not chunk:
                 break
             await audioqueue.put(chunk)
-            # TODO: cooldown if the queue can't catch up to prevent memory being filled up with entire track
+            if audioqueue.qsize() > 10:
+                await asyncio.sleep(0.1) # cooldown a bit
         await process.wait()
         await audioqueue.put('') # indicate EOF
         LOGGER.info("streaming of track_id %s completed" % track_id)
index 7ba28a95d2c9b2a6cf8ff54a2dd7bda558609427..1e0b9993530224bf19c9f5e840ac8acf0fb6d46d 100644 (file)
@@ -268,7 +268,7 @@ class QobuzProvider(MusicProvider):
         async with aiohttp.ClientSession(loop=asyncio.get_event_loop(), connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
             async with session.get(streamdetails['url']) as resp:
                 while True:
-                    chunk = await resp.content.read(512000)
+                    chunk = await resp.content.read(64000)
                     if not chunk:
                         break
                     yield chunk
index 0fc19698a18447678fd17c6092b3f27704c868f9..6742a5e0ac5e53968b3f377f78cbbd4615485369 100644 (file)
@@ -253,7 +253,7 @@ class SpotifyProvider(MusicProvider):
         args = ['-n', 'temp', '-u', self._username, '-p', self._password, '--pass-through', '--single-track', track_id]
         process = await asyncio.create_subprocess_exec(spotty, *args, stdout=asyncio.subprocess.PIPE)
         while not process.stdout.at_eof():
-            chunk = await process.stdout.read(128000)
+            chunk = await process.stdout.read(32000)
             if not chunk:
                 break
             yield chunk
index 0a5120105ba5687a849c38a6c443b9fade865d62..0ca694c60dc7a9a019956642f9fcc5a9f9b856dd 100755 (executable)
@@ -49,13 +49,25 @@ class Player():
             cmd = 'pause' if player.state == PlayerState.Playing else 'play'
         if cmd == 'power' and (cmd_args == 'toggle' or not cmd_args):
             cmd_args = 'off' if player.powered else 'on'
+        if cmd == 'volume' and cmd_args == 'up':
+            cmd_args = player.volume_level + 2
+        elif cmd == 'volume' and cmd_args == 'down':
+            cmd_args = player.volume_level - 2
+        elif cmd == 'volume' and '+' in str(cmd_args):
+            cmd_args = player.volume_level + try_parse_int(cmd_args.replace('+',''))
+        elif cmd == 'volume' and '-' in str(cmd_args):
+            cmd_args = player.volume_level - try_parse_int(cmd_args.replace('-',''))
+        if cmd == 'mute' and (cmd_args == 'toggle' or not cmd_args):
+            cmd_args = 'off' if player.muted else 'on'
+        if cmd == 'volume' and cmd_args:
+            if try_parse_int(cmd_args) > 100:
+                cmd_args = 100
+            elif try_parse_int(cmd_args) < 0:
+                cmd_args = 0
         if cmd == 'volume' and player.is_group and player.settings['apply_group_volume']:
             # group volume
             return await self.__player_command_group_volume(player, cmd, cmd_args)
-        if cmd == 'volume' and (cmd_args == 'up' or '+' in str(cmd_args)):
-            cmd_args = player.volume_level + 2
-        elif cmd == 'volume' and (cmd_args == 'down' or '-' in str(cmd_args)):
-            cmd_args = player.volume_level - 2
+        
         # redirect playlist related commands to parent player
         if player.group_parent and cmd not in ['power', 'volume', 'mute']:
             return await self.player_command(player.group_parent, cmd, cmd_args)
@@ -75,7 +87,6 @@ class Player():
         # normal execution of command on player
         await prov.player_command(player_id, cmd, cmd_args)
         
-        
     async def __player_command_hass_integration(self, player, cmd, cmd_args):
         ''' handle hass integration in player command '''
         if not self.mass.hass:
@@ -99,19 +110,12 @@ class Player():
             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, cmd, cmd_args):
+    async def __player_command_group_volume(self, player, new_volume):
         ''' handle group volume '''
-        if cmd_args == 'up':
-            volume_dif = 2
-            volume_dif_percent = 1.02
-        elif cmd_args == 'up':
-            volume_dif = -2
-            volume_dif_percent = 0.98
-        else:
-            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
+        cur_volume = player.volume_level
+        new_volume = try_parse_int(new_volume)
+        volume_dif = new_volume - cur_volume
+        volume_dif_percent = volume_dif/cur_volume
         player_childs = [item for item in self._players.values() if item.group_parent == player.player_id]
         for child_player in player_childs:
             if child_player.enabled and child_player.powered:
index 5259f53dfb928455b57b223fe150f12f11ad637a..006eefa954a446eee624bd4a29f05598c3b75ac1 100644 (file)
@@ -124,7 +124,7 @@ class ChromecastProvider(PlayerProvider):
             await self.__queue_load(player_id, self._player_queue[player_id], cur_queue_index)
         elif queue_opt == 'next':
             # insert new items at current index +1
-            if len(self._player_queue[player_id]) > cur_queue_index:
+            if len(self._player_queue[player_id]) > cur_queue_index+1:
                 old_next_uri = self._player_queue[player_id][cur_queue_index+1].uri
             else:
                 old_next_uri = None
index 65da48f33006a41118854b5f085e8bc547548192..0e5dbc180aa81b80ba1ce6b07aae5d25e7835966 100644 (file)
@@ -11,7 +11,7 @@ from typing import List
 import random
 import sys
 import socket
-from utils import run_periodic, LOGGER, parse_track_title, try_parse_int, get_ip
+from utils import run_periodic, LOGGER, parse_track_title, try_parse_int, get_ip, get_hostname
 from models import PlayerProvider, MusicPlayer, PlayerState, MediaType, TrackQuality, AlbumType, Artist, Album, Track, Playlist
 from constants import CONF_ENABLED
 
@@ -30,6 +30,7 @@ def config_entries():
         (CONF_ENABLED, True, CONF_ENABLED)
         ]
 
+
 class PyLMSServer(PlayerProvider):
     ''' Python implementation of SlimProto server '''
 
@@ -49,14 +50,20 @@ class PyLMSServer(PlayerProvider):
         # start slimproto server
         mass.event_loop.create_task(asyncio.start_server(self.__handle_socket_client, '0.0.0.0', 3483))
         # setup discovery
-        listen = mass.event_loop.create_datagram_endpoint(
-                DiscoveryProtocol, local_addr=('0.0.0.0', 3483), 
-                family=socket.AF_INET, reuse_address=True, reuse_port=True,
-            allow_broadcast=True)
-        mass.event_loop.create_task(listen)
+        mass.event_loop.create_task(self.start_discovery())
 
      ### Provider specific implementation #####
 
+    async def start_discovery(self):
+        transport, protocol = await self.mass.event_loop.create_datagram_endpoint(
+            lambda: DiscoveryProtocol(self.mass.web._http_port),
+        local_addr=('0.0.0.0', 3483))
+        try:
+            while True:
+                await asyncio.sleep(60)  # serve forever
+        finally:
+            transport.close()
+
     async def player_command(self, player_id, cmd:str, cmd_args=None):
         ''' issue command on player (play, pause, next, previous, stop, power, volume, mute) '''
         if cmd == 'play':
@@ -112,9 +119,11 @@ class PyLMSServer(PlayerProvider):
 
     async def __queue_play(self, player_id, index, send_flush=False):
         ''' send play command to player '''
+        if not player_id in self._player_queue:
+            return
         if index == None:
             index = self._player_queue_index[player_id]
-        if len(self._player_queue[player_id]) >= index-1:
+        if len(self._player_queue[player_id]) >= index:
             track = self._player_queue[player_id][index]
             if send_flush:
                 self._lmsplayers[player_id].flush()
@@ -173,7 +182,7 @@ class PyLMSServer(PlayerProvider):
         player.volume_level = lms_player.volume_level
         player.cur_item_time = lms_player._elapsed_seconds
         if event == "disconnected":
-            player.enabled = False
+            return await self.mass.player.remove_player(player_id)
         elif event == "power":
             player.powered = event_data
         elif event == "state":
@@ -220,8 +229,9 @@ class PyLMSServer(PlayerProvider):
                     lms_player.dataReceived(data)
                 else:
                     break
-        except RuntimeError:
-            LOGGER.warning("connection lost")
+        except Exception as exc:
+            # connection lost ?
+            LOGGER.warning(exc)
         # disconnect
         heartbeat_task.cancel()
         asyncio.create_task(self.__handle_player_event(lms_player.player_id, 'disconnected'))
@@ -292,7 +302,7 @@ class PyLMSPlayer(object):
         self.send_frame(b"strm", data)
 
     def flush(self):
-        data = self.pack_stream(b"f", autostart=b"1", flags=0)
+        data = self.pack_stream(b"f", autostart=b"0", flags=0)
         self.send_frame(b"strm", data)
 
     def pause(self):
@@ -546,6 +556,107 @@ class PyLMSPlayer(object):
         LOGGER.info("UREQ received")
 
 
+
+# from http://wiki.slimdevices.com/index.php/SlimProtoTCPProtocol#HELO
+devices = {
+    2: 'squeezebox',
+    3: 'softsqueeze',
+    4: 'squeezebox2',
+    5: 'transporter',
+    6: 'softsqueeze3',
+    7: 'receiver',
+    8: 'squeezeslave',
+    9: 'controller',
+    10: 'boom',
+    11: 'softboom',
+    12: 'squeezeplay',
+    }
+
+
+class PyLMSVolume(object):
+
+    """ Represents a sound volume. This is an awful lot more complex than it
+    sounds. """
+
+    minimum = 0
+    maximum = 100
+    step = 1
+
+    # this map is taken from Slim::Player::Squeezebox2 in the squeezecenter source
+    # i don't know how much magic it contains, or any way I can test it
+    old_map = [
+        0, 1, 1, 1, 2, 2, 2, 3,  3,  4,
+        5, 5, 6, 6, 7, 8, 9, 9, 10, 11,
+        12, 13, 14, 15, 16, 16, 17, 18, 19, 20,
+        22, 23, 24, 25, 26, 27, 28, 29, 30, 32,
+        33, 34, 35, 37, 38, 39, 40, 42, 43, 44,
+        46, 47, 48, 50, 51, 53, 54, 56, 57, 59,
+        60, 61, 63, 65, 66, 68, 69, 71, 72, 74,
+        75, 77, 79, 80, 82, 84, 85, 87, 89, 90,
+        92, 94, 96, 97, 99, 101, 103, 104, 106, 108, 110,
+        112, 113, 115, 117, 119, 121, 123, 125, 127, 128
+        ];
+
+    # new gain parameters, from the same place
+    total_volume_range = -50 # dB
+    step_point = -1           # Number of steps, up from the bottom, where a 2nd volume ramp kicks in.
+    step_fraction = 1         # fraction of totalVolumeRange where alternate volume ramp kicks in.
+
+    def __init__(self):
+        self.volume = 50
+
+    def increment(self):
+        """ Increment the volume """
+        self.volume += self.step
+        if self.volume > self.maximum:
+            self.volume = self.maximum
+
+    def decrement(self):
+        """ Decrement the volume """
+        self.volume -= self.step
+        if self.volume < self.minimum:
+            self.volume = self.minimum
+
+    def old_gain(self):
+        """ Return the "Old" gain value as required by the squeezebox """
+        return self.old_map[self.volume]
+
+    def decibels(self):
+        """ Return the "new" gain value. """
+
+        step_db = self.total_volume_range * self.step_fraction
+        max_volume_db = 0 # different on the boom?
+
+        # Equation for a line:
+        # y = mx+b
+        # y1 = mx1+b, y2 = mx2+b.
+        # y2-y1 = m(x2 - x1)
+        # y2 = m(x2 - x1) + y1
+        slope_high = max_volume_db - step_db / (100.0 - self.step_point)
+        slope_low = step_db - self.total_volume_range / (self.step_point - 0.0)
+        x2 = self.volume
+        if (x2 > self.step_point):
+            m = slope_high
+            x1 = 100
+            y1 = max_volume_db
+        else:
+            m = slope_low
+            x1 = 0
+            y1 = self.total_volume_range
+        return m * (x2 - x1) + y1
+
+    def new_gain(self):
+        db = self.decibels()
+        floatmult = 10 ** (db/20.0)
+        # avoid rounding errors somehow
+        if -30 <= db <= 0:
+            return int(floatmult * (1 << 8) + 0.5) * (1<<8)
+        else:
+            return int((floatmult * (1<<16)) + 0.5)
+
+
+##### UDP DISCOVERY STUFF #############
+
 class Datagram(object):
 
     @classmethod
@@ -572,7 +683,7 @@ class ClientDiscoveryDatagram(Datagram):
     client = None
 
     def __init__(self, data):
-        s = struct.unpack('!cxBB8x6B', data)
+        s = struct.unpack('!cxBB8x6B', data.encode())
         assert  s[0] == 'd'
         self.device = s[1]
         self.firmware = hex(s[2])
@@ -586,7 +697,7 @@ class DiscoveryResponseDatagram(Datagram):
     def __init__(self, hostname, port):
         hostname = hostname[:16].encode("UTF-8")
         hostname += (16 - len(hostname)) * '\x00'
-        self.packet = struct.pack('!c16s', 'D', hostname)
+        self.packet = struct.pack('!c16s', 'D', hostname).decode()
 
 class TLVDiscoveryRequestDatagram(Datagram):
     
@@ -596,13 +707,14 @@ class TLVDiscoveryRequestDatagram(Datagram):
         idx = 1
         length = len(data)-5
         while idx <= length:
-            typ, l = struct.unpack_from("4sB", data, idx)
+            typ, l = struct.unpack_from("4sB", data.encode(), idx)
             if l:
                 val = data[idx+5:idx+5+l]
                 idx += 5+l
             else:
                 val = None
                 idx += 5
+            typ = typ.decode()
             requestdata[typ] = val
         self.data = requestdata
             
@@ -624,6 +736,9 @@ class TLVDiscoveryResponseDatagram(Datagram):
 
 class DiscoveryProtocol():
 
+    def __init__(self, web_port):
+        self.web_port = web_port
+    
     def connection_made(self, transport):
         self.transport = transport
         # Allow receiving multicast broadcasts
@@ -637,31 +752,26 @@ class DiscoveryProtocol():
         for typ, value in requestdata.items():
             if typ == 'NAME':
                 # send full host name - no truncation
-                value = 'macbook-marcel' # TODO
+                value = get_hostname()
             elif typ == 'IPAD':
                 # send ipaddress as a string only if it is set
-                value = '192.168.1.145' # TODO
+                value = get_ip()
                 # :todo: IPv6
                 if value == '0.0.0.0':
                     # do not send back an ip address
                     typ = None
             elif typ == 'JSON':
                 # send port as a string
-                json_port = 9000 # todo: web.service.port
+                json_port = self.web_port
                 value = str(json_port)
             elif typ == 'VERS':
                 # send server version
                  value = '7.9'
             elif typ == 'UUID':
                 # send server uuid
-                value = 'test'
-            # elif typ == 'JVID':
-            #     # not handle, just log the information
-            #     typ = None
-            #     log.msg("Jive: %x:%x:%x:%x:%x:%x:" % struct.unpack('>6B', value),
-            #             logLevel=logging.INFO)
+                value = 'musicassistant'
             else:
-                LOGGER.error('Unexpected information request: %r', typ)
+                LOGGER.debug('Unexpected information request: %r', typ)
                 typ = None
             if typ:
                 responsedata[typ] = value
@@ -670,9 +780,8 @@ class DiscoveryProtocol():
     def datagram_received(self, data, addr):
         try:
             data = data.decode()
-            LOGGER.info('Received %r from %s' % (data, addr))
             dgram = Datagram.decode(data)
-            LOGGER.info("Data received from %s: %s" % (addr, dgram))
+            LOGGER.debug("Data received from %s: %s" % (addr, dgram))
             if isinstance(dgram, ClientDiscoveryDatagram):
                 self.sendDiscoveryResponse(addr)
             elif isinstance(dgram, TLVDiscoveryRequestDatagram):
@@ -682,114 +791,12 @@ class DiscoveryProtocol():
             LOGGER.exception(exc)
 
     def sendDiscoveryResponse(self, addr):
-        dgram = DiscoveryResponseDatagram('macbook-marcel', 3483)
-        LOGGER.info("Sending discovery response %r" % (dgram.packet,))
+        dgram = DiscoveryResponseDatagram(get_hostname(), 3483)
+        LOGGER.debug("Sending discovery response %r" % (dgram.packet,))
         self.transport.sendto(dgram.packet.encode(), addr)
 
     def sendTLVDiscoveryResponse(self, resonsedata, addr):
         dgram = TLVDiscoveryResponseDatagram(resonsedata)
-        LOGGER.info("Sending discovery response %r" % (dgram.packet,))
+        LOGGER.debug("Sending discovery response %r" % (dgram.packet,))
         self.transport.sendto(dgram.packet.encode(), addr)
 
-
-
-
-
-
-
-# from http://wiki.slimdevices.com/index.php/SlimProtoTCPProtocol#HELO
-devices = {
-    2: 'squeezebox',
-    3: 'softsqueeze',
-    4: 'squeezebox2',
-    5: 'transporter',
-    6: 'softsqueeze3',
-    7: 'receiver',
-    8: 'squeezeslave',
-    9: 'controller',
-    10: 'boom',
-    11: 'softboom',
-    12: 'squeezeplay',
-    }
-
-
-class PyLMSVolume(object):
-
-    """ Represents a sound volume. This is an awful lot more complex than it
-    sounds. """
-
-    minimum = 0
-    maximum = 100
-    step = 1
-
-    # this map is taken from Slim::Player::Squeezebox2 in the squeezecenter source
-    # i don't know how much magic it contains, or any way I can test it
-    old_map = [
-        0, 1, 1, 1, 2, 2, 2, 3,  3,  4,
-        5, 5, 6, 6, 7, 8, 9, 9, 10, 11,
-        12, 13, 14, 15, 16, 16, 17, 18, 19, 20,
-        22, 23, 24, 25, 26, 27, 28, 29, 30, 32,
-        33, 34, 35, 37, 38, 39, 40, 42, 43, 44,
-        46, 47, 48, 50, 51, 53, 54, 56, 57, 59,
-        60, 61, 63, 65, 66, 68, 69, 71, 72, 74,
-        75, 77, 79, 80, 82, 84, 85, 87, 89, 90,
-        92, 94, 96, 97, 99, 101, 103, 104, 106, 108, 110,
-        112, 113, 115, 117, 119, 121, 123, 125, 127, 128
-        ];
-
-    # new gain parameters, from the same place
-    total_volume_range = -50 # dB
-    step_point = -1           # Number of steps, up from the bottom, where a 2nd volume ramp kicks in.
-    step_fraction = 1         # fraction of totalVolumeRange where alternate volume ramp kicks in.
-
-    def __init__(self):
-        self.volume = 50
-
-    def increment(self):
-        """ Increment the volume """
-        self.volume += self.step
-        if self.volume > self.maximum:
-            self.volume = self.maximum
-
-    def decrement(self):
-        """ Decrement the volume """
-        self.volume -= self.step
-        if self.volume < self.minimum:
-            self.volume = self.minimum
-
-    def old_gain(self):
-        """ Return the "Old" gain value as required by the squeezebox """
-        return self.old_map[self.volume]
-
-    def decibels(self):
-        """ Return the "new" gain value. """
-
-        step_db = self.total_volume_range * self.step_fraction
-        max_volume_db = 0 # different on the boom?
-
-        # Equation for a line:
-        # y = mx+b
-        # y1 = mx1+b, y2 = mx2+b.
-        # y2-y1 = m(x2 - x1)
-        # y2 = m(x2 - x1) + y1
-        slope_high = max_volume_db - step_db / (100.0 - self.step_point)
-        slope_low = step_db - self.total_volume_range / (self.step_point - 0.0)
-        x2 = self.volume
-        if (x2 > self.step_point):
-            m = slope_high
-            x1 = 100
-            y1 = max_volume_db
-        else:
-            m = slope_low
-            x1 = 0
-            y1 = self.total_volume_range
-        return m * (x2 - x1) + y1
-
-    def new_gain(self):
-        db = self.decibels()
-        floatmult = 10 ** (db/20.0)
-        # avoid rounding errors somehow
-        if -30 <= db <= 0:
-            return int(floatmult * (1 << 8) + 0.5) * (1<<8)
-        else:
-            return int((floatmult * (1<<16)) + 0.5)
\ No newline at end of file
index 3ceb4ac35a64a1a55222dab4e20cf60760243b0d..c5fc2cb76ce57afa3e87929479bd5df4d22a5485 100755 (executable)
@@ -3,7 +3,7 @@
 
 import asyncio
 import os
-from utils import run_periodic, LOGGER
+from utils import run_periodic, LOGGER, run_async_background_task
 import json
 import aiohttp
 from aiohttp import web
@@ -219,7 +219,8 @@ class Web():
             ws_msg = {"message": msg, "message_details": msg_details }
             try:
                 await ws.send_json(ws_msg, dumps=json_serializer)
-            except ConnectionResetError:
+            except Exception as exc:
+                LOGGER.error(exc)
                 await self.mass.remove_event_listener(cb_id)
 
         cb_id = await self.mass.add_event_listener(send_event)
@@ -273,17 +274,28 @@ class Web():
                                  headers={'Content-Type': 'audio/flac'})
         await resp.prepare(request)
         if request.method.upper() != 'HEAD':
-            async for chunk in self.mass.http_streamer.get_audio_stream(track_id, provider, player_id):
+            # stream audio
+            queue = asyncio.Queue()
+            run_async_background_task(
+                self.mass.bg_executor, self.mass.http_streamer.get_audio_stream, queue, track_id, provider, player_id)
+            while True:
+                chunk = await queue.get()
+                if not chunk:
+                    queue.task_done()
+                    break
                 await resp.write(chunk)
+                queue.task_done()
+            LOGGER.info("Finished streaming %s" % track_id)
         return resp
 
     async def json_rpc(self, request):
         ''' 
-            implement fake LMS jsonrpc interface 
+            implement LMS jsonrpc interface 
             for some compatability with tools that talk to lms
             only support for basic commands
         '''
         data = await request.json()
+        LOGGER.info("jsonrpc: %s" % data)
         params = data['params']
         player_id = params[0]
         cmds = params[1]
@@ -291,13 +303,14 @@ class Web():
         if cmd_str in ['play', 'pause', 'stop']:
             await self.mass.player.player_command(player_id, cmd_str)
         elif 'power' in cmd_str:
-            await self.mass.player.player_command(player_id, cmd_str, cmd_str[1])
+            args = cmds[1] if len(cmds) > 1 else None
+            await self.mass.player.player_command(player_id, cmd_str, args)
         elif cmd_str == 'playlist index +1':
             await self.mass.player.player_command(player_id, 'next')
         elif cmd_str == 'playlist index -1':
             await self.mass.player.player_command(player_id, 'previous')
         elif 'mixer volume' in cmd_str:
-            await self.mass.player.player_command(player_id, 'volume', cmd_str[2])
+            await self.mass.player.player_command(player_id, 'volume', cmds[2])
         elif cmd_str == 'mixer muting 1':
             await self.mass.player.player_command(player_id, 'mute', 'on')
         elif cmd_str == 'mixer muting 0':
index 6e56c61525af857183566b06af098dfeeaf64d39..75b90cb1c878ab51cc18fb9107166d5125f44ff5 100755 (executable)
@@ -30,12 +30,12 @@ def run_background_task(executor, corofn, *args):
 def run_async_background_task(executor, corofn, *args):
     ''' run async task in background '''
     def run_task(corofn, *args):
-        LOGGER.info('running %s in background task' % corofn.__name__)
+        LOGGER.debug('running %s in background task' % corofn.__name__)
         new_loop = asyncio.new_event_loop()
         coro = corofn(*args)
         res = new_loop.run_until_complete(coro)
         new_loop.close()
-        LOGGER.info('completed %s in background task' % corofn.__name__)
+        LOGGER.debug('completed %s in background task' % corofn.__name__)
         return res
     return asyncio.get_event_loop().run_in_executor(executor, run_task, corofn, *args)
 
@@ -103,4 +103,7 @@ def get_ip():
         IP = '127.0.0.1'
     finally:
         s.close()
-    return IP
\ No newline at end of file
+    return IP
+
+def get_hostname():
+    return socket.gethostname()
\ No newline at end of file
index ba8e71f92889c4e27b4dd31b180ef6b1e36b43d7..cf49828fd318c6bf570d50df431f434ae894d4e5 100755 (executable)
@@ -251,6 +251,8 @@ Vue.component("player", {
         var players = [];
         if (msg.message == 'player updated')
           players = [msg.message_details];
+        else if (msg.message == 'player removed')
+          this.players[msg.message_details].enabled = false;
         else if (msg.message == 'players')
           players = msg.message_details;
         
@@ -262,14 +264,14 @@ Vue.component("player", {
 
         // select new active player
         // TODO: store previous player in local storage
-        if (!this.active_player_id)
+        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) {
               // prefer the first playing player
               this.active_player_id = player_id;
               break; 
             }
-        if (!this.active_player_id)
+            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)