From: marcelveldt Date: Tue, 14 May 2019 10:24:48 +0000 (+0200) Subject: improve hass integration X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=043f4a62779b1b427007e98f15b819e0ecab8160;p=music-assistant-server.git improve hass integration --- diff --git a/.gitignore b/.gitignore index 452d444d..4e202d18 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ music_assistant/config.json *.cert *.pem +music_assistant/testrun.sh diff --git a/Dockerfile b/Dockerfile index a72525f2..cf37ea7d 100755 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/config.json b/config.json index ef4eddad..4868e218 100755 --- a/config.json +++ b/config.json @@ -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 index 61552888..00000000 --- a/music_assistant/api.py +++ /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 diff --git a/music_assistant/main.py b/music_assistant/main.py index ad3835da..6f66ef49 100755 --- a/music_assistant/main.py +++ b/music_assistant/main.py @@ -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 diff --git a/music_assistant/modules/homeassistant.py b/music_assistant/modules/homeassistant.py index ad553937..699906b7 100644 --- a/music_assistant/modules/homeassistant.py +++ b/music_assistant/modules/homeassistant.py @@ -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', '', '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) diff --git a/music_assistant/modules/playerproviders/lms.py b/music_assistant/modules/playerproviders/lms.py index ab086a72..85d26a21 100644 --- a/music_assistant/modules/playerproviders/lms.py +++ b/music_assistant/modules/playerproviders/lms.py @@ -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 index 00000000..fbb9520e --- /dev/null +++ b/music_assistant/modules/web.py @@ -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 diff --git a/music_assistant/player.py b/music_assistant/player.py index 444ba1b6..8a295c19 100755 --- a/music_assistant/player.py +++ b/music_assistant/player.py @@ -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) diff --git a/music_assistant/web/pages/config.vue.js b/music_assistant/web/pages/config.vue.js index ff2e8c36..55c2e166 100755 --- a/music_assistant/web/pages/config.vue.js +++ b/music_assistant/web/pages/config.vue.js @@ -22,9 +22,6 @@ var Config = Vue.component('Config', {