improve hass integration
authormarcelveldt <marcelvanderveldt@MacBook-Pro.local>
Tue, 14 May 2019 10:24:48 +0000 (12:24 +0200)
committermarcelveldt <marcelvanderveldt@MacBook-Pro.local>
Tue, 14 May 2019 10:24:48 +0000 (12:24 +0200)
.gitignore
Dockerfile
config.json
music_assistant/api.py [deleted file]
music_assistant/main.py
music_assistant/modules/homeassistant.py
music_assistant/modules/playerproviders/lms.py
music_assistant/modules/web.py [new file with mode: 0755]
music_assistant/player.py
music_assistant/web/pages/config.vue.js

index 452d444da62bd7b24062bf43672d2a7371d0f723..4e202d180ca29cd1f7f82f3312754f55872878ed 100644 (file)
@@ -5,3 +5,4 @@
 music_assistant/config.json
 *.cert
 *.pem
+music_assistant/testrun.sh
index a72525f2c0f432b1d1f5eeedf5793a876ae56022..cf37ea7d3b52fc57f4045f50c483c54af27bfc7c 100755 (executable)
@@ -1,14 +1,15 @@
 FROM python:3.7.3-alpine
 
 # install deps
-RUN pip install --upgrade requirements.txt
+RUN apk add build-base python-dev flac sox taglib-dev
+COPY requirements.txt requirements.txt
+RUN pip install --upgrade -r requirements.txt
 
 # copy files
 RUN mkdir -p /usr/src/app
 WORKDIR /usr/src/app
 COPY music_assistant /usr/src/app
 RUN chmod a+x /usr/src/app/main.py
-RUN pip install --upgrade -r requirements.txt
 
 VOLUME ["/data"]
 
index ef4eddad1feca9b4713cc593e4eb15d5222cc8bf..4868e218062ec012c8c9a71ec6a73d0300d9fde9 100755 (executable)
@@ -1,11 +1,13 @@
 {
   "name": "Music Assistant",
-  "version": "0.0.1",
+  "version": "0.0.4",
   "description": "Media library manager for (streaming) media",
   "slug": "music_assistant",
   "startup": "application",
   "boot": "auto",
-  "map": [],
+  "arch": ["amd64"],
+  "map": ["share:rw","ssl"],
+  "webui": "http://[HOST]:[PORT:8095]",
   "host_network": true,
   "options": {
   },
diff --git a/music_assistant/api.py b/music_assistant/api.py
deleted file mode 100755 (executable)
index 6155288..0000000
+++ /dev/null
@@ -1,257 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding:utf-8 -*-
-
-import asyncio
-import os
-from utils import run_periodic, LOGGER
-import json
-import aiohttp
-from aiohttp import web
-from models import MediaType, media_type_from_string
-from functools import partial
-json_serializer = partial(json.dumps, default=lambda x: x.__dict__)
-import ssl
-
-class Api():
-    ''' expose our data through json api '''
-    
-    def __init__(self, mass, ssl_cert, ssl_key):
-        self.mass = mass
-        self._ssl_cert = ssl_cert
-        self._ssl_key = ssl_key
-        self.http_session = aiohttp.ClientSession()
-        mass.event_loop.create_task(self.setup_web())
-
-    def stop(self):
-        self.runner.cleanup()
-        self.http_session.close()
-
-    async def setup_web(self):
-        app = web.Application()
-        app.add_routes([web.get('/ws', self.websocket_handler)])
-        app.add_routes([web.get('/stream/{provider}/{track_id}', self.stream)])
-        app.add_routes([web.get('/api/search', self.search)])
-        app.add_routes([web.get('/api/config', self.get_config)])
-        app.add_routes([web.post('/api/config', self.save_config)])
-        app.add_routes([web.get('/api/players', self.players)])
-        app.add_routes([web.get('/api/players/{player_id}/queue', self.player_queue)])
-        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)])
-        app.add_routes([web.get('/api/players/{player_id}/play_media/{media_type}/{media_id}/{queue_opt}', self.play_media)])
-        app.add_routes([web.get('/api/playlists/{playlist_id}/tracks', self.playlist_tracks)])
-        app.add_routes([web.get('/api/artists/{artist_id}/toptracks', self.artist_toptracks)])
-        app.add_routes([web.get('/api/artists/{artist_id}/albums', self.artist_albums)])
-        app.add_routes([web.get('/api/albums/{album_id}/tracks', self.album_tracks)])
-        app.add_routes([web.get('/api/{media_type}', self.get_items)])
-        app.add_routes([web.get('/api/{media_type}/{media_id}/{action}', self.get_item)])
-        app.add_routes([web.get('/api/{media_type}/{media_id}', self.get_item)])
-        app.add_routes([web.get('/', self.index)])
-        app.router.add_static("/", "./web")  
-        
-        self.runner = web.AppRunner(app)
-        await self.runner.setup()
-        ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
-        ssl_context.load_cert_chain(self._ssl_cert, self._ssl_key)
-        http_site = web.TCPSite(self.runner, '0.0.0.0', 8095)
-        https_site = web.TCPSite(self.runner, '0.0.0.0', 8096, ssl_context=ssl_context)
-        await http_site.start()
-        await https_site.start()
-
-    async def get_items(self, request):
-        ''' get multiple library items'''
-        media_type_str = request.match_info.get('media_type')
-        media_type = media_type_from_string(media_type_str)
-        limit = int(request.query.get('limit', 50))
-        offset = int(request.query.get('offset', 0))
-        orderby = request.query.get('orderby', 'name')
-        provider_filter = request.rel_url.query.get('provider')
-        result = await self.mass.music.library_items(media_type, 
-                    limit=limit, offset=offset, 
-                    orderby=orderby, provider_filter=provider_filter)
-        return web.json_response(result, dumps=json_serializer)
-
-    async def get_item(self, request):
-        ''' get item full details'''
-        media_type_str = request.match_info.get('media_type')
-        media_type = media_type_from_string(media_type_str)
-        media_id = request.match_info.get('media_id')
-        action = request.match_info.get('action','')
-        lazy = request.rel_url.query.get('lazy', '') != 'false'
-        provider = request.rel_url.query.get('provider')
-        if action:
-            result = await self.mass.music.item_action(media_id, media_type, provider, action)
-        else:
-            result = await self.mass.music.item(media_id, media_type, provider, lazy=lazy)
-        return web.json_response(result, dumps=json_serializer)
-
-    async def artist_toptracks(self, request):
-        ''' get top tracks for given artist '''
-        artist_id = request.match_info.get('artist_id')
-        provider = request.rel_url.query.get('provider')
-        result = await self.mass.music.artist_toptracks(artist_id, provider)
-        return web.json_response(result, dumps=json_serializer)
-
-    async def artist_albums(self, request):
-        ''' get (all) albums for given artist '''
-        artist_id = request.match_info.get('artist_id')
-        provider = request.rel_url.query.get('provider')
-        result = await self.mass.music.artist_albums(artist_id, provider)
-        return web.json_response(result, dumps=json_serializer)
-
-    async def playlist_tracks(self, request):
-        ''' get playlist tracks from provider'''
-        playlist_id = request.match_info.get('playlist_id')
-        limit = int(request.query.get('limit', 50))
-        offset = int(request.query.get('offset', 0))
-        provider = request.rel_url.query.get('provider')
-        result = await self.mass.music.playlist_tracks(playlist_id, provider, offset=offset, limit=limit)
-        return web.json_response(result, dumps=json_serializer)
-
-    async def album_tracks(self, request):
-        ''' get album tracks from provider'''
-        album_id = request.match_info.get('album_id')
-        provider = request.rel_url.query.get('provider')
-        result = await self.mass.music.album_tracks(album_id, provider)
-        return web.json_response(result, dumps=json_serializer)
-
-    async def search(self, request):
-        ''' search database or providers '''
-        searchquery = request.rel_url.query.get('query')
-        media_types_query = request.rel_url.query.get('media_types')
-        limit = request.rel_url.query.get('media_id', 5)
-        online = request.rel_url.query.get('online', False)
-        media_types = []
-        if not media_types_query or "artists" in media_types_query:
-            media_types.append(MediaType.Artist)
-        if not media_types_query or "albums" in media_types_query:
-            media_types.append(MediaType.Album)
-        if not media_types_query or "tracks" in media_types_query:
-            media_types.append(MediaType.Track)
-        if not media_types_query or "playlists" in media_types_query:
-            media_types.append(MediaType.Playlist)
-        # get results from database
-        result = await self.mass.music.search(searchquery, media_types, limit=limit, online=online)
-        return web.json_response(result, dumps=json_serializer)
-
-    async def players(self, request):
-        ''' get all players '''
-        players = await self.mass.player.players()
-        return web.json_response(players, dumps=json_serializer)
-
-    async def player_command(self, request):
-        ''' issue player command'''
-        player_id = request.match_info.get('player_id')
-        cmd = request.match_info.get('cmd')
-        cmd_args = request.match_info.get('cmd_args')
-        result = await self.mass.player.player_command(player_id, cmd, cmd_args)
-        return web.json_response(result, dumps=json_serializer) 
-    
-    async def play_media(self, request):
-        ''' issue player play_media command'''
-        player_id = request.match_info.get('player_id')
-        media_type_str = request.match_info.get('media_type')
-        media_type = media_type_from_string(media_type_str)
-        media_id = request.match_info.get('media_id')
-        queue_opt = request.match_info.get('queue_opt','')
-        provider = request.rel_url.query.get('provider')
-        media_item = await self.mass.music.item(media_id, media_type, provider, lazy=True)
-        result = await self.mass.player.play_media(player_id, media_item, queue_opt)
-        return web.json_response(result, dumps=json_serializer) 
-    
-    async def player_queue(self, request):
-        ''' return the items in the player's queue '''
-        player_id = request.match_info.get('player_id')
-        limit = int(request.query.get('limit', 50))
-        offset = int(request.query.get('offset', 0))
-        result = await self.mass.player.player_queue(player_id, offset, limit)
-        return web.json_response(result, dumps=json_serializer) 
-    
-    async def index(self, request):  
-        return web.FileResponse("./web/index.html")
-
-    async def websocket_handler(self, request):
-        ''' websockets handler '''
-        ws = web.WebSocketResponse()
-        await ws.prepare(request)
-        # register callback for internal events
-        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)
-            except Exception as exc:
-                if 'the handler is closed' in str(exc):
-                    await self.mass.remove_event_listener(cb_id)
-                else:
-                    LOGGER.exception(exc)
-
-        cb_id = await self.mass.add_event_listener(send_event)
-        # process incoming messages
-        async for msg in ws:
-            if msg.type == aiohttp.WSMsgType.TEXT:
-                if msg.data == 'close':
-                    await self.mass.remove_event_listener(cb_id)
-                    await ws.close()
-                else:
-                    # for now we only use WS for player commands
-                    if msg.data == 'players':
-                        players = await self.mass.player.players()
-                        ws_msg = {'message': 'players', 'message_details': players}
-                        await ws.send_json(ws_msg, dumps=json_serializer)
-                    # elif msg.data.startswith('players') and '/play_media/' in msg.data:
-                    #     #'players/{player_id}/play_media/{media_type}/{media_id}/{queue_opt}'
-                    #     msg_data_parts = msg.data.split('/')
-                    #     player_id = msg_data_parts[1]
-                    #     media_type = msg_data_parts[3]
-                    #     media_type = media_type_from_string(media_type)
-                    #     media_id = msg_data_parts[4]
-                    #     queue_opt = msg_data_parts[5] if len(msg_data_parts) == 6 else 'replace'
-                    #     media_item = await self.mass.music.item(media_id, media_type, lazy=True)
-                    #     await self.mass.player.play_media(player_id, media_item, queue_opt)
-
-                    elif msg.data.startswith('players') and '/cmd/' in msg.data:
-                        # players/{player_id}/cmd/{cmd} or players/{player_id}/cmd/{cmd}/{cmd_args}
-                        msg_data_parts = msg.data.split('/')
-                        player_id = msg_data_parts[1]
-                        cmd = msg_data_parts[3]
-                        cmd_args = msg_data_parts[4] if len(msg_data_parts) == 5 else None
-                        await self.mass.player.player_command(player_id, cmd, cmd_args)
-            elif msg.type == aiohttp.WSMsgType.ERROR:
-                LOGGER.error('ws connection closed with exception %s' %
-                    ws.exception())
-        LOGGER.info('websocket connection closed')
-        return ws
-
-    async def get_config(self, request):
-        ''' get the config '''
-        return web.json_response(self.mass.config)
-
-    async def save_config(self, request):
-        ''' save the config '''
-        LOGGER.debug('save config called from api')
-        new_config = await request.json()
-        for key, value in self.mass.config.items():
-            if isinstance(value, dict):
-                for subkey, subvalue in value.items():
-                    if subkey in new_config[key]:
-                        self.mass.config[key][subkey] = new_config[key][subkey]
-            elif key in new_config:
-                self.mass.config[key] = new_config[key]
-        self.mass.save_config()
-        return web.Response(text='success')
-
-    async def stream(self, request):
-        ''' start streaming audio from provider '''
-        track_id = request.match_info.get('track_id')
-        provider = request.match_info.get('provider')
-        #stream_details = await self.mass.music.providers[provider].get_stream_details(track_id)
-        # resp = web.StreamResponse(status=200,
-        #                         reason='OK',
-        #                         headers={'Content-Type': stream_details['mime_type']})
-        resp = web.StreamResponse(status=200,
-                                 reason='OK',
-                                 headers={'Content-Type': 'audio/flac'})
-        await resp.prepare(request)
-        async for chunk in self.mass.music.providers[provider].get_stream(track_id):
-            await resp.write(chunk)
-        return resp
\ No newline at end of file
index ad3835da199155c029f3535cd3415a0c67ad82f7..6f66ef49b225fd8edef73d6cd4095156b1e95cff 100755 (executable)
@@ -15,16 +15,16 @@ import time
 
 from database import Database
 from metadata import MetaData
-from api import Api
 from utils import run_periodic, LOGGER
 from cache import Cache
 from music import Music
 from player import Player
 from modules.homeassistant import setup as hass_setup
+from modules.web import setup as web_setup
 
 class Main():
 
-    def __init__(self, datapath, ssl_cert, ssl_key):
+    def __init__(self, datapath):
         uvloop.install()
         self._datapath = datapath
         self.parse_config()
@@ -40,12 +40,12 @@ class Main():
         self.db = Database(datapath, self.event_loop)
         # allow some time for the database to initialize
         while not self.db.db_ready:
-            time.sleep(0.5) 
+            time.sleep(0.15)
         self.cache = Cache(datapath)
         self.metadata = MetaData(self.event_loop, self.db, self.cache)
 
         # init modules
-        self.api = Api(self, ssl_cert, ssl_key)
+        self.web = web_setup(self)
         self.hass = hass_setup(self)
         self.music = Music(self)
         self.player = Player(self)
@@ -112,16 +112,14 @@ class Main():
         ''' properly close all connections'''
         print('stop requested!')
         self.save_config()
-        self.api.stop()
+        self.web.stop()
         print('stopping event loop...')
         self.event_loop.stop()
         self.event_loop.close()
 
 if __name__ == "__main__":
-    datapath = sys.argv[1:]
+    datapath = sys.argv[1]
     if not datapath:
         datapath = os.path.dirname(os.path.abspath(__file__))
-    ssl_cert = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'certificate.cert')
-    ssl_key = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'privkey.pem')
-    Main(datapath, ssl_cert, ssl_key)
+    Main(datapath)
     
\ No newline at end of file
index ad5539370d82676350f7f807a73789f1a89724dd..699906b7929ccc17a0477889d33687a9c38a7930 100644 (file)
@@ -29,40 +29,42 @@ import slugify as slug
 
 def setup(mass):
     ''' setup the module and read/apply config'''
-    if not mass.config['base'].get('homeassistant'):
-        mass.config['base']['homeassistant'] = {}
+    create_config_entries(mass.config)
     conf = mass.config['base']['homeassistant']
-    conf['__desc__'] = config_entries()
-    for key, def_value, desc in config_entries():
-        if not key in conf:
-            conf[key] = def_value
     enabled = conf.get(CONF_ENABLED)
     token = conf.get('token')
     url = conf.get('url')
     if enabled and url and token:
-        # append hass player config settings
-        hass_player_conf = [("hass_power_entity", "", "Attach player power to homeassistant entity"),
-                        ("hass_power_entity_source", "", "Source on the homeassistant entity (optional)"),
-                        ("hass_volume_entity", "", "Attach player volume to homeassistant entity")]
-        for key, default, desc in hass_player_conf:
-            entry_found = False
-            for value in mass.config['player_settings']['__desc__']:
-                if value[0] == key:
-                    entry_found = True
-                    break
-            if not entry_found:
-                mass.config['player_settings']['__desc__'].append((key, default, desc))
         return HomeAssistant(mass, url, token)
     return None
 
-def config_entries():
+def create_config_entries(config):
     ''' get the config entries for this module (list with key/value pairs)'''
-    return [
+    config_entries = [
         (CONF_ENABLED, False, CONF_ENABLED),
         ('url', 'localhost', 'URL to homeassistant (e.g. https://homeassistant:8123)'), 
         ('token', '<password>', 'Long Lived Access Token'),
         ('publish_players', True, 'Publish players to Home Assistant')
         ]
+    if not config['base'].get('homeassistant'):
+        config['base']['homeassistant'] = {}
+    config['base']['homeassistant']['__desc__'] = config_entries
+    for key, def_value, desc in config_entries:
+        if not key in config['base']['homeassistant']:
+            config['base']['homeassistant'][key] = def_value
+    # append hass player config settings
+    if config['base']['homeassistant'][CONF_ENABLED]:
+        hass_player_conf = [("hass_power_entity", "", "Attach player power to homeassistant entity"),
+                        ("hass_power_entity_source", "", "Source on the homeassistant entity (optional)"),
+                        ("hass_volume_entity", "", "Attach player volume to homeassistant entity")]
+        for key, default, desc in hass_player_conf:
+            entry_found = False
+            for value in config['player_settings']['__desc__']:
+                if value[0] == key:
+                    entry_found = True
+                    break
+            if not entry_found:
+                config['player_settings']['__desc__'].append((key, default, desc))
 
 class HomeAssistant():
     ''' HomeAssistant integration '''
@@ -135,8 +137,15 @@ class HomeAssistant():
                     await self.mass.player.player_command(player_id, 'power', 'on')
                 elif service == 'turn_off':
                     await self.mass.player.player_command(player_id, 'power', 'off')
+                elif service == 'toggle':
+                    await self.mass.player.player_command(player_id, 'power', 'toggle')
                 elif service == 'volume_mute':
-                    await self.mass.player.player_command(player_id, 'mute', service_data['is_volume_muted'])
+                    args = 'on' if service_data['is_volume_muted'] else 'off'
+                    await self.mass.player.player_command(player_id, 'mute', args)
+                elif service == 'volume_up':
+                    await self.mass.player.player_command(player_id, 'volume', 'up')
+                elif service == 'volume_down':
+                    await self.mass.player.player_command(player_id, 'volume', 'down')
                 elif service == 'volume_set':
                     volume_level = service_data['volume_level']*100
                     await self.mass.player.player_command(player_id, 'volume', volume_level)
index ab086a72d4ed29a5a41758db47de825ca696c517..85d26a2121d79d4fec59bcad146ab9c2de730003 100644 (file)
@@ -187,7 +187,8 @@ class LMSProvider(PlayerProvider):
                 # player is a groupplayer, retrieve childs
                 group_player_child_ids = await self.__get_group_childs(player_id)
                 for child_player_id in group_player_child_ids:
-                    self._players[child_player_id].group_parent = player_id
+                    if child_player_id in self._players:
+                        self._players[child_player_id].group_parent = player_id
             elif player.group_parent:
                 # check if player parent is still correct
                 group_player_child_ids = await self.__get_group_childs(player.group_parent)
diff --git a/music_assistant/modules/web.py b/music_assistant/modules/web.py
new file mode 100755 (executable)
index 0000000..fbb9520
--- /dev/null
@@ -0,0 +1,288 @@
+#!/usr/bin/env python3
+# -*- coding:utf-8 -*-
+
+import asyncio
+import os
+from utils import run_periodic, LOGGER
+import json
+import aiohttp
+from aiohttp import web
+from models import MediaType, media_type_from_string
+from functools import partial
+json_serializer = partial(json.dumps, default=lambda x: x.__dict__)
+import ssl
+
+def setup(mass):
+    ''' setup the module and read/apply config'''
+    create_config_entries(mass.config)
+    conf = mass.config['base']['web']
+    if conf['ssl_certificate'] and os.path.isfile(conf['ssl_certificate']):
+        ssl_cert = conf['ssl_certificate']
+    else:
+        ssl_cert = ''
+    if conf['ssl_key'] and os.path.isfile(conf['ssl_key']):
+        ssl_key = conf['ssl_key']
+    else:
+        ssl_key = ''
+    hostname = conf['hostname']
+    return Web(mass, ssl_cert, ssl_key, hostname)
+
+def create_config_entries(config):
+    ''' get the config entries for this module (list with key/value pairs)'''
+    config_entries = [
+        ('ssl_certificate', '', 'Path to ssl certificate file'), 
+        ('ssl_key', '', 'Path to ssl keyfile'),
+        ('hostname', '', 'Hostname (FQDN used in the certificate)')
+        ]
+    if not config['base'].get('web'):
+        config['base']['web'] = {}
+    config['base']['web']['__desc__'] = config_entries
+    for key, def_value, desc in config_entries:
+        if not key in config['base']['web']:
+            config['base']['web'][key] = def_value
+
+class Web():
+    ''' webserver and json/websocket api '''
+    
+    def __init__(self, mass, ssl_cert, ssl_key, hostname):
+        self.mass = mass
+        self._ssl_cert = ssl_cert
+        self._ssl_key = ssl_key
+        self._hostname = hostname
+        self.http_session = aiohttp.ClientSession()
+        mass.event_loop.create_task(self.setup_web())
+
+    def stop(self):
+        asyncio.create_task(self.runner.cleanup())
+        asyncio.create_task(self.http_session.close())
+
+    async def setup_web(self):
+        app = web.Application()
+        app.add_routes([web.get('/ws', self.websocket_handler)])
+        app.add_routes([web.get('/stream/{provider}/{track_id}', self.stream)])
+        app.add_routes([web.get('/api/search', self.search)])
+        app.add_routes([web.get('/api/config', self.get_config)])
+        app.add_routes([web.post('/api/config', self.save_config)])
+        app.add_routes([web.get('/api/players', self.players)])
+        app.add_routes([web.get('/api/players/{player_id}/queue', self.player_queue)])
+        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)])
+        app.add_routes([web.get('/api/players/{player_id}/play_media/{media_type}/{media_id}/{queue_opt}', self.play_media)])
+        app.add_routes([web.get('/api/playlists/{playlist_id}/tracks', self.playlist_tracks)])
+        app.add_routes([web.get('/api/artists/{artist_id}/toptracks', self.artist_toptracks)])
+        app.add_routes([web.get('/api/artists/{artist_id}/albums', self.artist_albums)])
+        app.add_routes([web.get('/api/albums/{album_id}/tracks', self.album_tracks)])
+        app.add_routes([web.get('/api/{media_type}', self.get_items)])
+        app.add_routes([web.get('/api/{media_type}/{media_id}/{action}', self.get_item)])
+        app.add_routes([web.get('/api/{media_type}/{media_id}', self.get_item)])
+        app.add_routes([web.get('/', self.index)])
+        app.router.add_static("/", "./web")  
+        
+        self.runner = web.AppRunner(app)
+        await self.runner.setup()
+        http_site = web.TCPSite(self.runner, '0.0.0.0', 8095)
+        await http_site.start()
+        if self._ssl_cert and self._ssl_key:
+            ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+            ssl_context.load_cert_chain(self._ssl_cert, self._ssl_key)
+            https_site = web.TCPSite(self.runner, '0.0.0.0', 8096, ssl_context=ssl_context)
+            await https_site.start()
+
+    async def get_items(self, request):
+        ''' get multiple library items'''
+        media_type_str = request.match_info.get('media_type')
+        media_type = media_type_from_string(media_type_str)
+        limit = int(request.query.get('limit', 50))
+        offset = int(request.query.get('offset', 0))
+        orderby = request.query.get('orderby', 'name')
+        provider_filter = request.rel_url.query.get('provider')
+        result = await self.mass.music.library_items(media_type, 
+                    limit=limit, offset=offset, 
+                    orderby=orderby, provider_filter=provider_filter)
+        return web.json_response(result, dumps=json_serializer)
+
+    async def get_item(self, request):
+        ''' get item full details'''
+        media_type_str = request.match_info.get('media_type')
+        media_type = media_type_from_string(media_type_str)
+        media_id = request.match_info.get('media_id')
+        action = request.match_info.get('action','')
+        lazy = request.rel_url.query.get('lazy', '') != 'false'
+        provider = request.rel_url.query.get('provider')
+        if action:
+            result = await self.mass.music.item_action(media_id, media_type, provider, action)
+        else:
+            result = await self.mass.music.item(media_id, media_type, provider, lazy=lazy)
+        return web.json_response(result, dumps=json_serializer)
+
+    async def artist_toptracks(self, request):
+        ''' get top tracks for given artist '''
+        artist_id = request.match_info.get('artist_id')
+        provider = request.rel_url.query.get('provider')
+        result = await self.mass.music.artist_toptracks(artist_id, provider)
+        return web.json_response(result, dumps=json_serializer)
+
+    async def artist_albums(self, request):
+        ''' get (all) albums for given artist '''
+        artist_id = request.match_info.get('artist_id')
+        provider = request.rel_url.query.get('provider')
+        result = await self.mass.music.artist_albums(artist_id, provider)
+        return web.json_response(result, dumps=json_serializer)
+
+    async def playlist_tracks(self, request):
+        ''' get playlist tracks from provider'''
+        playlist_id = request.match_info.get('playlist_id')
+        limit = int(request.query.get('limit', 50))
+        offset = int(request.query.get('offset', 0))
+        provider = request.rel_url.query.get('provider')
+        result = await self.mass.music.playlist_tracks(playlist_id, provider, offset=offset, limit=limit)
+        return web.json_response(result, dumps=json_serializer)
+
+    async def album_tracks(self, request):
+        ''' get album tracks from provider'''
+        album_id = request.match_info.get('album_id')
+        provider = request.rel_url.query.get('provider')
+        result = await self.mass.music.album_tracks(album_id, provider)
+        return web.json_response(result, dumps=json_serializer)
+
+    async def search(self, request):
+        ''' search database or providers '''
+        searchquery = request.rel_url.query.get('query')
+        media_types_query = request.rel_url.query.get('media_types')
+        limit = request.rel_url.query.get('media_id', 5)
+        online = request.rel_url.query.get('online', False)
+        media_types = []
+        if not media_types_query or "artists" in media_types_query:
+            media_types.append(MediaType.Artist)
+        if not media_types_query or "albums" in media_types_query:
+            media_types.append(MediaType.Album)
+        if not media_types_query or "tracks" in media_types_query:
+            media_types.append(MediaType.Track)
+        if not media_types_query or "playlists" in media_types_query:
+            media_types.append(MediaType.Playlist)
+        # get results from database
+        result = await self.mass.music.search(searchquery, media_types, limit=limit, online=online)
+        return web.json_response(result, dumps=json_serializer)
+
+    async def players(self, request):
+        ''' get all players '''
+        players = await self.mass.player.players()
+        return web.json_response(players, dumps=json_serializer)
+
+    async def player_command(self, request):
+        ''' issue player command'''
+        player_id = request.match_info.get('player_id')
+        cmd = request.match_info.get('cmd')
+        cmd_args = request.match_info.get('cmd_args')
+        result = await self.mass.player.player_command(player_id, cmd, cmd_args)
+        return web.json_response(result, dumps=json_serializer) 
+    
+    async def play_media(self, request):
+        ''' issue player play_media command'''
+        player_id = request.match_info.get('player_id')
+        media_type_str = request.match_info.get('media_type')
+        media_type = media_type_from_string(media_type_str)
+        media_id = request.match_info.get('media_id')
+        queue_opt = request.match_info.get('queue_opt','')
+        provider = request.rel_url.query.get('provider')
+        media_item = await self.mass.music.item(media_id, media_type, provider, lazy=True)
+        result = await self.mass.player.play_media(player_id, media_item, queue_opt)
+        return web.json_response(result, dumps=json_serializer) 
+    
+    async def player_queue(self, request):
+        ''' return the items in the player's queue '''
+        player_id = request.match_info.get('player_id')
+        limit = int(request.query.get('limit', 50))
+        offset = int(request.query.get('offset', 0))
+        result = await self.mass.player.player_queue(player_id, offset, limit)
+        return web.json_response(result, dumps=json_serializer) 
+    
+    async def index(self, request):  
+        return web.FileResponse("./web/index.html")
+
+    async def websocket_handler(self, request):
+        ''' websockets handler '''
+        ws = web.WebSocketResponse()
+        await ws.prepare(request)
+        # register callback for internal events
+        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)
+            except Exception as exc:
+                if 'the handler is closed' in str(exc):
+                    await self.mass.remove_event_listener(cb_id)
+                else:
+                    LOGGER.exception(exc)
+
+        cb_id = await self.mass.add_event_listener(send_event)
+        # process incoming messages
+        async for msg in ws:
+            if msg.type == aiohttp.WSMsgType.TEXT:
+                if msg.data == 'close':
+                    await self.mass.remove_event_listener(cb_id)
+                    await ws.close()
+                else:
+                    # for now we only use WS for player commands
+                    if msg.data == 'players':
+                        players = await self.mass.player.players()
+                        ws_msg = {'message': 'players', 'message_details': players}
+                        await ws.send_json(ws_msg, dumps=json_serializer)
+                    # elif msg.data.startswith('players') and '/play_media/' in msg.data:
+                    #     #'players/{player_id}/play_media/{media_type}/{media_id}/{queue_opt}'
+                    #     msg_data_parts = msg.data.split('/')
+                    #     player_id = msg_data_parts[1]
+                    #     media_type = msg_data_parts[3]
+                    #     media_type = media_type_from_string(media_type)
+                    #     media_id = msg_data_parts[4]
+                    #     queue_opt = msg_data_parts[5] if len(msg_data_parts) == 6 else 'replace'
+                    #     media_item = await self.mass.music.item(media_id, media_type, lazy=True)
+                    #     await self.mass.player.play_media(player_id, media_item, queue_opt)
+
+                    elif msg.data.startswith('players') and '/cmd/' in msg.data:
+                        # players/{player_id}/cmd/{cmd} or players/{player_id}/cmd/{cmd}/{cmd_args}
+                        msg_data_parts = msg.data.split('/')
+                        player_id = msg_data_parts[1]
+                        cmd = msg_data_parts[3]
+                        cmd_args = msg_data_parts[4] if len(msg_data_parts) == 5 else None
+                        await self.mass.player.player_command(player_id, cmd, cmd_args)
+            elif msg.type == aiohttp.WSMsgType.ERROR:
+                LOGGER.error('ws connection closed with exception %s' %
+                    ws.exception())
+        LOGGER.info('websocket connection closed')
+        return ws
+
+    async def get_config(self, request):
+        ''' get the config '''
+        return web.json_response(self.mass.config)
+
+    async def save_config(self, request):
+        ''' save the config '''
+        LOGGER.debug('save config called from api')
+        new_config = await request.json()
+        for key, value in self.mass.config.items():
+            if isinstance(value, dict):
+                for subkey, subvalue in value.items():
+                    if subkey in new_config[key]:
+                        self.mass.config[key][subkey] = new_config[key][subkey]
+            elif key in new_config:
+                self.mass.config[key] = new_config[key]
+        self.mass.save_config()
+        return web.Response(text='success')
+
+    async def stream(self, request):
+        ''' start streaming audio from provider '''
+        track_id = request.match_info.get('track_id')
+        provider = request.match_info.get('provider')
+        #stream_details = await self.mass.music.providers[provider].get_stream_details(track_id)
+        # resp = web.StreamResponse(status=200,
+        #                         reason='OK',
+        #                         headers={'Content-Type': stream_details['mime_type']})
+        resp = web.StreamResponse(status=200,
+                                 reason='OK',
+                                 headers={'Content-Type': 'audio/flac'})
+        await resp.prepare(request)
+        async for chunk in self.mass.music.providers[provider].get_stream(track_id):
+            await resp.write(chunk)
+        return resp
\ No newline at end of file
index 444ba1b62557d004b9c5c01aebc11adbdbeb4bc2..8a295c19b904018b5f36d3e1a2f42b94c18ceac2 100755 (executable)
@@ -45,10 +45,12 @@ class Player():
         # handle some common workarounds
         if cmd in ['pause', 'play'] and cmd_args == 'toggle':
             cmd = 'pause' if player.state == PlayerState.Playing else 'play'
+        if cmd == 'power' and cmd_args == 'toggle':
+            cmd_args = 'off' if player.powered else 'on'
         if cmd == 'volume' and cmd_args == 'up':
-            cmd_args = try_parse_int(cmd_args) + 2
+            cmd_args = player.volume_level + 2
         elif cmd == 'volume' and cmd_args == 'down':
-            cmd_args = try_parse_int(cmd_args) - 2
+            cmd_args = player.volume_level - 2
         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)
index ff2e8c36b604b9ae3bb86eb8101800e9a613d370..55c2e16671b7cff2d6484c572f42b7925ce2604f 100755 (executable)
@@ -22,9 +22,6 @@ var Config = Vue.component('Config', {
             </template>
             <template v-for="(conf_value, conf_key) in conf.base">
                 <v-list-tile>
-                  <v-list-tile-avatar>
-                      <img :src="'images/icons/' + conf_key + '.png'"/>
-                  </v-list-tile-avatar>
                   <v-list-tile-content>
                     <v-list-tile-title class="title">{{ conf_key }}</v-list-tile-title>
                   </v-list-tile-content>