From: Marcel van der Veldt Date: Thu, 27 Jun 2019 19:40:21 +0000 (+0200) Subject: some refactoring X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=30cf7346d188318d8e87ea1babb9ef4592096c74;p=music-assistant-server.git some refactoring --- diff --git a/music_assistant/main.py b/music_assistant/main.py index 778bedaf..e9e5530c 100755 --- a/music_assistant/main.py +++ b/music_assistant/main.py @@ -61,24 +61,31 @@ class Main(): self.event_loop.run_forever() except (KeyboardInterrupt, SystemExit): LOGGER.info('Exit requested!') + self.signal_event("system_shutdown") + self.event_loop.stop() self.save_config() + time.sleep(5) self.event_loop.close() LOGGER.info('Shutdown complete.') - async def event(self, msg, msg_details=None): - ''' signal event ''' + def signal_event(self, msg, msg_details=None): + ''' signal (systemwide) event ''' LOGGER.debug("Event: %s - %s" %(msg, msg_details)) listeners = list(self.event_listeners.values()) - for listener in listeners: - await listener(msg, msg_details) + for callback, eventfilter in listeners: + if not eventfilter or eventfilter in msg: + if not asyncio.iscoroutinefunction(callback): + callback(msg, msg_details) + else: + self.event_loop.create_task(callback(msg, msg_details)) - async def add_event_listener(self, cb): + def add_event_listener(self, cb, eventfilter=None): ''' add callback to our event listeners ''' cb_id = str(uuid.uuid4()) - self.event_listeners[cb_id] = cb + self.event_listeners[cb_id] = (cb, eventfilter) return cb_id - async def remove_event_listener(self, cb_id): + def remove_event_listener(self, cb_id): ''' remove callback from our event listeners ''' self.event_listeners.pop(cb_id, None) diff --git a/music_assistant/modules/homeassistant.py b/music_assistant/modules/homeassistant.py index 28b61f77..32961b47 100644 --- a/music_assistant/modules/homeassistant.py +++ b/music_assistant/modules/homeassistant.py @@ -72,7 +72,7 @@ class HomeAssistant(): self.__last_id = 10 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)) + self.mass.add_event_listener(self.mass_event, "player updated") mass.event_loop.create_task(self.__get_sources()) async def get_state(self, entity_id, attribute='state', register_listener=None): diff --git a/music_assistant/modules/http_streamer.py b/music_assistant/modules/http_streamer.py index fcb6ccbf..19ff243d 100755 --- a/music_assistant/modules/http_streamer.py +++ b/music_assistant/modules/http_streamer.py @@ -196,13 +196,13 @@ class HTTPStreamer(): queue_tracks = await self.mass.player.player_queue(player_id, queue_index, queue_index+1) queue_track = queue_tracks[0] except IndexError: - LOGGER.info("queue index out of range or end reached") + LOGGER.warning("queue index out of range or end reached") break params = urllib.parse.parse_qs(queue_track.uri.split('?')[1]) track_id = params['track_id'][0] provider = params['provider'][0] - LOGGER.info("Start Streaming queue track: %s (%s) on player %s" % (track_id, queue_track.name, player.name)) + LOGGER.debug("Start Streaming queue track: %s (%s) on player %s" % (track_id, queue_track.name, player.name)) fade_in_part = b'' cur_chunk = 0 prev_chunk = None @@ -248,7 +248,10 @@ class HTTPStreamer(): stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE) last_part, stderr = await process.communicate(prev_chunk + chunk) if len(last_part) < fade_bytes: - # not enough data for crossfade duration + # not enough data for crossfade duration after the strip action... + last_part = prev_chunk + chunk + if len(last_part) < fade_bytes: + # still not enough data so we'll skip the crossfading LOGGER.warning("not enough data for fadeout so skip crossfade... %s" % len(last_part)) sox_proc.stdin.write(last_part) bytes_written += len(last_part) @@ -291,7 +294,7 @@ class HTTPStreamer(): # move to next queue index queue_index += 1 self.mass.event_loop.create_task(self.mass.player.player_queue_stream_update(player_id, queue_index, False)) - LOGGER.info("Finished Streaming queue track: %s (%s) on player %s" % (track_id, queue_track.name, player.name)) + LOGGER.debug("Finished Streaming queue track: %s (%s) on player %s" % (track_id, queue_track.name, player.name)) # end of queue reached, pass last fadeout bits to final output if last_fadeout_data and not cancelled.is_set(): sox_proc.stdin.write(last_fadeout_data) @@ -315,15 +318,18 @@ class HTTPStreamer(): sox_effects += ' rate -v %s' % resample # stream audio from provider streamdetails = asyncio.run_coroutine_threadsafe( - self.mass.music.providers[provider].get_stream_details(track_id), self.mass.event_loop).result() + self.mass.music.providers[provider].get_stream_details(track_id), + self.mass.event_loop).result() if not streamdetails: yield (True, b'') return # TODO: add support for AAC streams (which sox doesn't natively support) if streamdetails['type'] == 'url': - args = 'sox -t %s "%s" -t %s - %s %s' % (streamdetails["content_type"], streamdetails["path"], outputfmt, gain_correct, sox_effects) + args = 'sox -t %s "%s" -t %s - %s %s' % (streamdetails["content_type"], + streamdetails["path"], outputfmt, gain_correct, sox_effects) elif streamdetails['type'] == 'executable': - args = '%s | sox -t %s - -t %s - %s %s' % (streamdetails["path"], streamdetails["content_type"], outputfmt, gain_correct, sox_effects) + args = '%s | sox -t %s - -t %s - %s %s' % (streamdetails["path"], + streamdetails["content_type"], outputfmt, gain_correct, sox_effects) LOGGER.debug("Running sox with args: %s" % args) process = await asyncio.create_subprocess_shell(args, stdout=asyncio.subprocess.PIPE) @@ -331,7 +337,7 @@ class HTTPStreamer(): streamdetails["provider"] = provider streamdetails["track_id"] = track_id streamdetails["player_id"] = player_id - self.mass.event_loop.create_task(self.mass.event('streaming_started', streamdetails)) + self.mass.signal_event('streaming_started', streamdetails) # yield chunks from stdout # we keep 1 chunk behind to detect end of stream properly prev_chunk = b'' @@ -355,7 +361,7 @@ class HTTPStreamer(): if cancelled.is_set(): LOGGER.warning("__get_audio_stream for track_id %s interrupted" % track_id) else: - LOGGER.info("__get_audio_stream for track_id %s completed" % track_id) + LOGGER.debug("__get_audio_stream for track_id %s completed" % track_id) # fire event that streaming has ended for this track (needed by some streaming providers) if resample: bytes_per_second = resample * (32/8) * 2 @@ -363,7 +369,7 @@ class HTTPStreamer(): bytes_per_second = streamdetails["sample_rate"] * (streamdetails["bit_depth"]/8) * 2 seconds_streamed = int(bytes_sent/bytes_per_second) streamdetails["seconds"] = seconds_streamed - self.mass.event_loop.create_task(self.mass.event('streaming_ended', streamdetails)) + self.mass.signal_event('streaming_ended', streamdetails) # send task to background to analyse the audio self.mass.event_loop.create_task(self.__analyze_audio(track_id, provider)) diff --git a/music_assistant/modules/music.py b/music_assistant/modules/music.py index 04b62f22..ad4bdc14 100755 --- a/music_assistant/modules/music.py +++ b/music_assistant/modules/music.py @@ -195,11 +195,11 @@ class Music(): items = list(toolz.unique(items, key=operator.attrgetter('item_id'))) return result - async def item_action(self, item_id, media_type, provider='database', action=None): + async def item_action(self, item_id, media_type, provider, action, action_details=None): ''' perform action on item (such as library add/remove) ''' result = None item = await self.item(item_id, media_type, provider) - if item and action in ['add', 'remove']: + if item and action in ['library_add', 'library_remove']: # remove or add item to the library for prov_mapping in result.provider_ids: prov_id = prov_mapping['provider'] diff --git a/music_assistant/modules/musicproviders/qobuz.py b/music_assistant/modules/musicproviders/qobuz.py index 41570930..b1051fa3 100644 --- a/music_assistant/modules/musicproviders/qobuz.py +++ b/music_assistant/modules/musicproviders/qobuz.py @@ -50,7 +50,8 @@ class QobuzProvider(MusicProvider): self.__app_secret = "47249d0eaefa6bf43a959c09aacdbce8" # TEMP! Own key requested self.__logged_in = False self.throttler = Throttler(rate_limit=2, period=1) - mass.event_loop.create_task(mass.add_event_listener(self.mass_event)) + mass.add_event_listener(self.mass_event, 'streaming_started') + mass.add_event_listener(self.mass_event, 'streaming_ended') async def search(self, searchstring, media_types=List[MediaType], limit=5): ''' perform search on the provider ''' @@ -262,8 +263,6 @@ class QobuzProvider(MusicProvider): streamdetails = await self.__get_data('track/getFileUrl', params, sign_request=True, ignore_cache=True) if streamdetails and streamdetails.get('url'): break - else: - await asyncio.sleep(1) if not streamdetails or not streamdetails.get('url'): LOGGER.error("Unable to retrieve stream url for track %s" % track_id) return None @@ -305,7 +304,7 @@ class QobuzProvider(MusicProvider): await self.__post_data("track/reportStreamingStart", data=events) async def __parse_artist(self, artist_obj): - ''' parse spotify artist object to generic layout ''' + ''' parse qobuz artist object to generic layout ''' artist = Artist() if not artist_obj.get('id'): return None @@ -329,7 +328,7 @@ class QobuzProvider(MusicProvider): return artist async def __parse_album(self, album_obj): - ''' parse spotify album object to generic layout ''' + ''' parse qobuz album object to generic layout ''' album = Album() if not album_obj.get('id') or not album_obj["streamable"] or not album_obj["displayable"]: # some safety checks @@ -375,7 +374,7 @@ class QobuzProvider(MusicProvider): return album async def __parse_track(self, track_obj): - ''' parse spotify track object to generic layout ''' + ''' parse qobuz track object to generic layout ''' track = Track() if not track_obj.get('id') or not track_obj["streamable"] or not track_obj["displayable"]: # some safety checks @@ -450,7 +449,7 @@ class QobuzProvider(MusicProvider): return track async def __parse_playlist(self, playlist_obj): - ''' parse spotify playlist object to generic layout ''' + ''' parse qobuz playlist object to generic layout ''' playlist = Playlist() if not playlist_obj.get('id'): return None @@ -538,8 +537,8 @@ class QobuzProvider(MusicProvider): result = await response.json() if not result or 'error' in result: LOGGER.error(url) - LOGGER.error(params) - LOGGER.error(result) + LOGGER.debug(params) + LOGGER.debug(result) return None return result except Exception as exc: @@ -555,7 +554,7 @@ class QobuzProvider(MusicProvider): result = await response.json() if not result or 'error' in result: LOGGER.error(url) - LOGGER.error(params) - LOGGER.error(result) + LOGGER.debug(params) + LOGGER.debug(result) result = None return result \ No newline at end of file diff --git a/music_assistant/modules/player.py b/music_assistant/modules/player.py index 52fd7a68..da0e50a4 100755 --- a/music_assistant/modules/player.py +++ b/music_assistant/modules/player.py @@ -156,7 +156,7 @@ class Player(): async def remove_player(self, player_id): ''' handle a player remove ''' self._players.pop(player_id, None) - asyncio.ensure_future(self.mass.event('player removed', player_id)) + self.mass.signal_event('player removed', player_id) async def trigger_update(self, player_id): ''' manually trigger update for a player ''' @@ -212,7 +212,7 @@ class Player(): 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)) + self.mass.signal_event('player updated', player) if player_details.is_group: # is groupplayer, trigger update of its childs player_childs = [item for item in self._players.values() if item.group_parent == player_id] diff --git a/music_assistant/modules/playerproviders/chromecast.py b/music_assistant/modules/playerproviders/chromecast.py index 877235df..00776259 100644 --- a/music_assistant/modules/playerproviders/chromecast.py +++ b/music_assistant/modules/playerproviders/chromecast.py @@ -55,7 +55,11 @@ class ChromecastProvider(PlayerProvider): self._player_queue_index = {} self._player_queue_stream_startindex = {} self.supported_musicproviders = ['http'] - run_background_task(self.mass.bg_executor, self.__chromecast_discovery) + abort_discovery = self.__chromecast_discovery() + def on_shutdown(msg, msg_details): + LOGGER.info('stopping Chromecast discovery...') + abort_discovery() + mass.add_event_listener(on_shutdown, 'system_shutdown') ### Provider specific implementation ##### @@ -395,14 +399,16 @@ class ChromecastProvider(PlayerProvider): from pychromecast.discovery import start_discovery, stop_discovery def internal_callback(name): """Called when zeroconf has discovered a new chromecast.""" - self.__chromecast_discovered(listener.services[name]) + #self.__chromecast_discovered(listener.services[name]) + asyncio.run_coroutine_threadsafe( + self.__chromecast_discovered(listener.services[name]), self.mass.event_loop) def internal_stop(): """Stops discovery of new chromecasts.""" stop_discovery(browser) listener, browser = start_discovery(internal_callback) return internal_stop - def __chromecast_discovered(self, discovery_info): + async def __chromecast_discovered(self, discovery_info): ''' callback when a (new) chromecast device is discovered ''' ip_address, port, uuid, model_name, friendly_name = discovery_info player_id = str(uuid) diff --git a/music_assistant/modules/web.py b/music_assistant/modules/web.py index 033ba947..7701d7f1 100755 --- a/music_assistant/modules/web.py +++ b/music_assistant/modules/web.py @@ -119,10 +119,11 @@ class Web(): media_type = media_type_from_string(media_type_str) media_id = request.match_info.get('media_id') action = request.match_info.get('action','') + action_details = request.rel_url.query.get('action_details') 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) + result = await self.mass.music.item_action(media_id, media_type, provider, action, action_details) else: result = await self.mass.music.item(media_id, media_type, provider, lazy=lazy) return web.json_response(result, dumps=json_serializer) @@ -225,7 +226,7 @@ class Web(): async def send_event(msg, msg_details): ws_msg = {"message": msg, "message_details": msg_details } await ws.send_json(ws_msg, dumps=json_serializer) - cb_id = await self.mass.add_event_listener(send_event) + cb_id = self.mass.add_event_listener(send_event) # process incoming messages async for msg in ws: if msg.type != aiohttp.WSMsgType.TEXT: @@ -243,8 +244,8 @@ class Web(): 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) finally: - await self.mass.remove_event_listener(cb_id) - LOGGER.info('websocket connection closed') + self.mass.remove_event_listener(cb_id) + LOGGER.debug('websocket connection closed') return ws async def get_config(self, request): @@ -252,17 +253,24 @@ class Web(): return web.json_response(self.mass.config) async def save_config(self, request): - ''' save the config ''' + ''' save (partial) config ''' LOGGER.debug('save config called from api') new_config = await request.json() + config_changed = False 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] + if self.mass.config[key][subkey] != new_config[key][subkey]: + config_changed = True + 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() + if self.mass.config[key] != new_config[key]: + config_changed = True + self.mass.config[key] = new_config[key] + if config_changed: + self.mass.save_config() + self.mass.signal_event('config_changed') return web.Response(text='success') async def json_rpc(self, request): @@ -298,5 +306,7 @@ class Web(): await self.mass.player.player_command(player_id, 'volume', 'down') elif cmd_str == 'button power': await self.mass.player.player_command(player_id, 'power', 'toggle') + else: + return web.Response(text='command not supported') return web.Response(text='success') \ No newline at end of file diff --git a/music_assistant/temp.flac b/music_assistant/temp.flac deleted file mode 100644 index add58392..00000000 Binary files a/music_assistant/temp.flac and /dev/null differ diff --git a/music_assistant/web/components/playmenu.vue.js b/music_assistant/web/components/playmenu.vue.js index cbe4433b..611ecc36 100644 --- a/music_assistant/web/components/playmenu.vue.js +++ b/music_assistant/web/components/playmenu.vue.js @@ -3,7 +3,7 @@ Vue.component("playmenu", { - {{ !!$globals.playmenuitem ? $globals.playmenuitem.name : 'nix' }} + {{ !!$globals.playmenuitem ? $globals.playmenuitem.name : '' }} {{ $t('play_on') }} {{ active_player.name }} @@ -45,6 +45,26 @@ Vue.component("playmenu", { + + + + add_circle_outline + + + {{ $t('add_playlist') }} + + + + + + + remove_circle_outline + + + {{ $t('remove_playlist') }} + + + @@ -62,12 +82,12 @@ Vue.component("playmenu", { created() { }, methods: { itemClick(cmd) { - if (cmd == 'info') + if (cmd == 'info') this.$router.push({ path: '/tracks/' + this.$globals.playmenuitem.item_id, query: {provider: this.$globals.playmenuitem.provider}}) else this.$emit('playItem', this.$globals.playmenuitem, cmd) // close dialog this.$globals.showplaymenu = false; - }, + }, } }) diff --git a/music_assistant/web/config_old.vue.js b/music_assistant/web/config_old.vue.js deleted file mode 100755 index 77299c23..00000000 --- a/music_assistant/web/config_old.vue.js +++ /dev/null @@ -1,202 +0,0 @@ -var Config = Vue.component('Config', { - template: ` -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - Save - -
- `, - props: [], - data() { - return { - conf: {}, - players: {} - } - }, - computed: { - playersLst() - { - var playersLst = []; - for (player_id in this.conf.player_settings) - if (player_id != '__desc__') - playersLst.push({id: player_id, name: this.conf.player_settings[player_id].name}) - return playersLst; - } - - }, - created() { - this.$globals.windowtitle = this.$t('settings'); - this.getPlayers(); - this.getConfig(); - console.log(this.$globals.all_players); - }, - methods: { - getConfig () { - axios - .get('/api/config') - .then(result => { - this.conf = result.data; - }) - .catch(error => { - console.log("error", error); - }); - }, - saveConfig () { - axios - .post('/api/config', this.conf) - .then(result => { - console.log(result); - }) - .catch(error => { - console.log("error", error); - }); - }, - getPlayers () { - const api_url = '/api/players'; - axios - .get(api_url) - .then(result => { - for (var item of result.data) - this.$set(this.players, item.player_id, item) - }) - .catch(error => { - console.log("error", error); - this.showProgress = false; - }); - }, - } -}) diff --git a/music_assistant/web/index.html b/music_assistant/web/index.html index 14a90a3d..0584f5c4 100755 --- a/music_assistant/web/index.html +++ b/music_assistant/web/index.html @@ -86,9 +86,9 @@ function toggleLibrary (item) { var endpoint = "/api/" + item.media_type + "/"; item_id = item.item_id.toString(); - var action = "/remove" + var action = "/library_remove" if (item.in_library.length == 0) - action = "/add" + action = "/library_add" var url = endpoint + item_id + action; console.log('loading ' + url); axios @@ -96,7 +96,7 @@ .then(result => { data = result.data; console.log(data); - if (action == "/remove") + if (action == "/library_remove") item.in_library = [] else item.in_library = [provider] diff --git a/music_assistant/web/strings.js b/music_assistant/web/strings.js index b4011f6a..5c1df076 100644 --- a/music_assistant/web/strings.js +++ b/music_assistant/web/strings.js @@ -16,6 +16,8 @@ const messages = { type_to_search: "Type here to search...", add_library: "Add to library", remove_library: "Remove from library", + add_playlist: "Add to playlist...", + remove_playlist: "Remove from playlist", // settings strings conf: { enabled: "Enabled", @@ -102,6 +104,8 @@ const messages = { type_to_search: "Type hier om te zoeken...", add_library: "Voeg toe aan bibliotheek", remove_library: "Verwijder uit bibliotheek", + add_playlist: "Aan playlist toevoegen...", + remove_playlist: "Verwijder uit playlist", // settings strings conf: { enabled: "Ingeschakeld",