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