music_assistant/config.json
*.cert
*.pem
+music_assistant/testrun.sh
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"]
{
"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": {
},
+++ /dev/null
-#!/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
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()
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)
''' 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
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 '''
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)
# 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)
--- /dev/null
+#!/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
# 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)
</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>