allow hot reloading of modules
authormarcelveldt <marcelvanderveldt@MacBook-Pro.local>
Tue, 12 Nov 2019 20:49:51 +0000 (21:49 +0100)
committermarcelveldt <marcelvanderveldt@MacBook-Pro.local>
Tue, 12 Nov 2019 20:49:51 +0000 (21:49 +0100)
15 files changed:
music_assistant/__init__.py
music_assistant/models/musicprovider.py
music_assistant/models/playerprovider.py
music_assistant/music_manager.py
music_assistant/musicproviders/file.py
music_assistant/musicproviders/qobuz.py
music_assistant/musicproviders/spotify.py
music_assistant/musicproviders/tunein.py
music_assistant/player_manager.py
music_assistant/playerproviders/chromecast.py
music_assistant/playerproviders/sonos.py
music_assistant/playerproviders/squeezebox.py
music_assistant/playerproviders/webplayer.py
music_assistant/utils.py
music_assistant/web.py

index 51684a17185700169da51fbbc3a680ec866dc844..6e16371d8bb07f83fc64dda717447d33b09e62b5 100644 (file)
@@ -65,6 +65,7 @@ class MusicAssistant():
         except asyncio.CancelledError:
             LOGGER.info("Application shutdown")
             await self.signal_event("shutdown")
+            self.config.save()
             await self.db.close()
             await self.cache.close()
 
index dc7f4cdbf9cf9bd46f954e13f93d12a52df6edd6..f2b68c443bbf1256945beecfc4f894facdf52dec 100755 (executable)
@@ -17,17 +17,16 @@ class MusicProvider():
         Uses a form of lazy provisioning to local db as cache
     """
 
-    name = 'My great Music provider'  # display name
-    prov_id = 'my_provider'  # used as id
-    icon = ''
-
     def __init__(self, mass):
+        """[DO NOT OVERRIDE]"""
+        self.prov_id = ''
+        self.name = ''
         self.mass = mass
         self.cache = mass.cache
 
-    async def setup(self):
-        """ async initialize of module """
-        pass
+    async def setup(self, conf):
+        """[SHOULD OVERRIDE] Setup the provider"""
+        return False
 
     ### Common methods and properties ####
 
index 24d8704223eb23c672a71eb9de52f55aa88be2ed..edc194d84aa531c124745282302f259ae8b60dfd 100755 (executable)
@@ -17,16 +17,20 @@ class PlayerProvider():
         Common methods usable for every provider
         Provider specific methods should be overriden in the provider specific implementation
     '''
-    
 
-    def __init__(self, mass, conf):
+    def __init__(self, mass):
+        """[DO NOT OVERRIDE]"""
+        self.prov_id = ''
+        self.name = ''
         self.mass = mass
-        self.name = 'My great Musicplayer provider' # display name
-        self.prov_id = 'my_provider' # used as id
-        self.player_config_entries = [] # player specific config entries
+        self.cache = mass.cache
+        self.player_config_entries = []
 
-    ### Common methods and properties ####
+    async def setup(self, conf):
+        """[SHOULD OVERRIDE] Setup the provider"""
+        return False
 
+    ### Common methods and properties ####
 
     @property
     def players(self):
index dcbaecbd8c284eaeec2825432e923f6be0414977..f8b7d43fe47700a4b5bfd0d676a5027bca221ccb 100755 (executable)
@@ -48,17 +48,26 @@ class MusicManager():
     def __init__(self, mass):
         self.running_sync_jobs = []
         self.mass = mass
-        # dynamically load musicprovider modules
-        self.providers = load_provider_modules(mass, CONF_KEY_MUSICPROVIDERS)
+        self.providers = {}
 
     async def setup(self):
         ''' async initialize of module '''
-        # start providers
-        for prov in self.providers.values():
-            await prov.setup()
+        # load providers
+        await self.load_modules()
         # schedule sync task
         self.mass.event_loop.create_task(self.__sync_music_providers())
 
+    async def load_modules(self):
+        """Dynamically (un)load musicprovider modules."""
+        prev_ids = list(self.providers.keys())
+        await load_provider_modules(self.mass, 
+                self.providers, CONF_KEY_MUSICPROVIDERS)
+        # schedule sync for any newly added providers
+        for prov_id in self.providers:
+            if prov_id not in prev_ids:
+                self.mass.event_loop.create_task(
+                        self.sync_music_provider(prov_id))
+
     async def item(self,
                    item_id,
                    media_type: MediaType,
index 6d2650758fae831bae29d39ed5730d46cd3c1a2d..c577007974535cfa7e1ee694fbc96838cd033d64 100644 (file)
@@ -13,7 +13,6 @@ from ..utils import run_periodic, LOGGER, parse_title_and_version
 from ..models import MusicProvider, MediaType, TrackQuality, AlbumType, Artist, Album, Track, Playlist
 from ..constants import CONF_ENABLED
 
-PROV_ID = 'file'
 PROV_NAME = 'Local files and playlists'
 PROV_CLASS = 'FileProvider'
 
@@ -33,13 +32,9 @@ class FileProvider(MusicProvider):
         Supports having URI's from streaming providers within m3u playlist
         Should be compatible with LMS
     '''
-    
 
-    def __init__(self, mass, conf):
-        self.name = PROV_NAME
-        self.prov_id = PROV_ID
-        self.mass = mass
-        self.cache = mass.cache
+    async def setup(self, conf):
+        """ setup the provider, return True if succesfull"""
         self._music_dir = conf["music_dir"]
         self._playlists_dir = conf["playlists_dir"]
         if not os.path.isdir(conf["music_dir"]):
index 71cd4edd198eb1f4881c28546b1bb6fc720646e9..1c488a6631fa564d279e6d19fa957bcddb351dc7 100644 (file)
@@ -15,7 +15,6 @@ from ..models import MusicProvider, MediaType, TrackQuality, \
 from ..constants import CONF_USERNAME, CONF_PASSWORD, CONF_ENABLED, \
     CONF_TYPE_PASSWORD, EVENT_STREAM_STARTED, EVENT_PLAYBACK_STOPPED
 
-PROV_ID = 'qobuz'
 PROV_NAME = 'Qobuz'
 PROV_CLASS = 'QobuzProvider'
 
@@ -28,21 +27,19 @@ class QobuzProvider(MusicProvider):
 
     http_session = None
     throttler = None
+    __username = None
+    __password = None
+    __user_auth_info = None
+    __logged_in = None
 
-    def __init__(self, mass, conf):
-        ''' Support for streaming music provider Qobuz '''
-        super().__init__(mass)
-        self.name = PROV_NAME
-        self.prov_id = PROV_ID
+    async def setup(self, conf):
+        ''' perform async setup '''
         self.__username = conf[CONF_USERNAME]
         self.__password = conf[CONF_PASSWORD]
         if not conf[CONF_USERNAME] or not conf[CONF_PASSWORD]:
             raise Exception("Username and password must not be empty")
         self.__user_auth_info = None
         self.__logged_in = False
-
-    async def setup(self):
-        ''' perform async setup '''
         self.http_session = aiohttp.ClientSession(
             loop=self.mass.event_loop, connector=aiohttp.TCPConnector())
         self.throttler = Throttler(rate_limit=4, period=1)
index 0bf773958120cfc0f00829509508087688a563e9..85597e17099970416949bef34383295c6758ebca 100644 (file)
@@ -16,7 +16,6 @@ from ..models import MusicProvider, MediaType, TrackQuality, \
     AlbumType, Artist, Album, Track, Playlist
 from ..constants import CONF_USERNAME, CONF_PASSWORD, CONF_ENABLED, CONF_TYPE_PASSWORD
 
-PROV_ID = 'spotify'
 PROV_NAME = 'Spotify'
 PROV_CLASS = 'SpotifyProvider'
 
@@ -31,20 +30,14 @@ class SpotifyProvider(MusicProvider):
     http_session = None
     sp_user = None
 
-    def __init__(self, mass, conf):
-        ''' Support for streaming provider Spotify '''
-        super().__init__(mass)
-        self.name = PROV_NAME
-        self.prov_id = PROV_ID
+    async def setup(self, conf):
+        ''' perform async setup '''
         self._cur_user = None
         if not conf[CONF_USERNAME] or not conf[CONF_PASSWORD]:
             raise Exception("Username and password must not be empty")
         self._username = conf[CONF_USERNAME]
         self._password = conf[CONF_PASSWORD]
         self.__auth_token = {}
-
-    async def setup(self):
-        ''' perform async setup '''
         self.throttler = Throttler(rate_limit=4, period=1)
         self.http_session = aiohttp.ClientSession(
             loop=self.mass.event_loop, connector=aiohttp.TCPConnector())
@@ -216,8 +209,8 @@ class SpotifyProvider(MusicProvider):
             item = await self.track(prov_item_id)
         await self.mass.db.remove_from_library(item.item_id, media_type,
                                                self.prov_id)
-        LOGGER.debug("deleted item %s from %s - %s" %
-                     (prov_item_id, self.prov_id, result))
+        LOGGER.debug("deleted item %s from %s - %s",
+                     prov_item_id, self.prov_id, result)
 
     async def add_playlist_tracks(self, prov_playlist_id, prov_track_ids):
         ''' add track(s) to playlist '''
@@ -254,7 +247,7 @@ class SpotifyProvider(MusicProvider):
             "content_type": "ogg",
             "sample_rate": 44100,
             "bit_depth": 16,
-            "provider": PROV_ID,
+            "provider": self.prov_id,
             "item_id": track.item_id
         }
 
index d7988a8e17e11306019962a45cca39205e0aa434..4e59a8e3abc578fda101c999661a08caed0d627b 100644 (file)
@@ -1,21 +1,15 @@
 #!/usr/bin/env python3
 # -*- coding:utf-8 -*-
 
-import asyncio
-import os
 from typing import List
-import sys
-import time
 from asyncio_throttle import Throttler
-import json
 import aiohttp
 
-from ..utils import run_periodic, LOGGER
+from ..utils import LOGGER
 from ..models import MusicProvider, MediaType, TrackQuality, Radio
 from ..constants import CONF_USERNAME, CONF_PASSWORD, CONF_ENABLED, CONF_TYPE_PASSWORD
 
 
-PROV_ID = 'tunein'
 PROV_NAME = 'TuneIn Radio'
 PROV_CLASS = 'TuneInProvider'
 
@@ -26,20 +20,18 @@ CONFIG_ENTRIES = [
     ]
 
 class TuneInProvider(MusicProvider):
-    
 
-    def __init__(self, mass, conf):
-        ''' Support for streaming radio provider TuneIn '''
-        super().__init__(mass)
-        self.name = PROV_NAME
-        self.prov_id = PROV_ID
+    _username = None
+    _password = None
+    http_session = None
+    throttler = None
+
+    async def setup(self, conf):
+        ''' perform async setup '''
         if not conf[CONF_USERNAME] or not conf[CONF_PASSWORD]:
             raise Exception("Username and password must not be empty")
         self._username = conf[CONF_USERNAME]
         self._password = conf[CONF_PASSWORD]
-
-    async def setup(self):
-        ''' perform async setup '''
         self.http_session = aiohttp.ClientSession(
                 loop=self.mass.event_loop, connector=aiohttp.TCPConnector())
         self.throttler = Throttler(rate_limit=1, period=1)
index 48f0e4a868983e5fcc15ab67bee7fab66db289ae..9b61a98b0e3c4949dbd76a72cea2cf8493f87904 100755 (executable)
@@ -28,16 +28,19 @@ class PlayerManager():
     def __init__(self, mass):
         self.mass = mass
         self._players = {}
-        # dynamically load musicprovider modules
-        self.providers = load_provider_modules(mass, CONF_KEY_PLAYERPROVIDERS)
+        self.providers = {}
         
     async def setup(self):
         ''' async initialize of module '''
-        # start providers
-        for prov in self.providers.values():
-            await prov.setup()
+        # load providers
+        await self.load_modules()
         # register state listener
         await self.mass.add_event_listener(self.handle_mass_events, EVENT_HASS_ENTITY_CHANGED)
+
+    async def load_modules(self):
+        """Dynamically (un)load musicprovider modules."""
+        await load_provider_modules(self.mass, 
+                self.providers, CONF_KEY_PLAYERPROVIDERS)
     
     @property
     def players(self):
index e6cf2fac25d8a51b8fc5ea271eabfaaa43ce04a0..a9c5eda7d2575d1ee148e769f83b41cf4bd9290d 100644 (file)
@@ -36,7 +36,7 @@ class ChromecastPlayer(Player):
     def __init__(self, *args, **kwargs):
         self.__cc_report_progress_task = None
         super().__init__(*args, **kwargs)
-        
+
     def __del__(self):
         if self.__cc_report_progress_task:
             self.__cc_report_progress_task.cancel()
@@ -217,17 +217,13 @@ class ChromecastPlayer(Player):
 
 class ChromecastProvider(PlayerProvider):
     ''' support for ChromeCast Audio '''
+    _discovery_running = False
     
-    def __init__(self, mass, conf):
-        super().__init__(mass, conf)
-        self.prov_id = PROV_ID
-        self.name = PROV_NAME
+    async def setup(self, conf):
+        ''' perform async setup '''
         self._discovery_running = False
         logging.getLogger('pychromecast').setLevel(logging.WARNING)
         self.player_config_entries = PLAYER_CONFIG_ENTRIES
-
-    async def setup(self):
-        ''' perform async setup '''
         self.mass.event_loop.create_task(
                 self.__periodic_chromecast_discovery())
 
index b77f32bec124f4dacc0d8620db73727eee5c8201..2e9b6d3540bfe72d56f2a433d9ea4ed647f54f51 100644 (file)
@@ -23,8 +23,6 @@ CONFIG_ENTRIES = [
     (CONF_ENABLED, True, CONF_ENABLED),
     ]
 
-PLAYER_CONFIG_ENTRIES = []
-
 class SonosPlayer(Player):
     ''' Sonos player object '''
 
@@ -163,15 +161,9 @@ class SonosPlayer(Player):
 
 class SonosProvider(PlayerProvider):
     ''' support for Sonos speakers '''
-    
-    def __init__(self, mass, conf):
-        super().__init__(mass, conf)
-        self.prov_id = PROV_ID
-        self.name = PROV_NAME
-        self._discovery_running = False
-        self.player_config_entries = PLAYER_CONFIG_ENTRIES
-
-    async def setup(self):
+    _discovery_running = False
+
+    async def setup(self, conf):
         ''' perform async setup '''
         self.mass.event_loop.create_task(
                 self.__periodic_discovery())
index 0581dbf40f48a8e6fc87c3d3491af3b1f7d30ad6..94b05691c74ac6b7154d37cc5617249341b75217 100644 (file)
@@ -24,21 +24,13 @@ CONFIG_ENTRIES = [
     (CONF_ENABLED, True, CONF_ENABLED),
     ]
 
-PLAYER_CONFIG_ENTRIES = []
-
 
 class PySqueezeProvider(PlayerProvider):
     ''' Python implementation of SlimProto server '''
 
-    def __init__(self, mass, conf):
-        super().__init__(mass, conf)
-        self.prov_id = PROV_ID
-        self.name = PROV_NAME
-        self.player_config_entries = PLAYER_CONFIG_ENTRIES
-
      ### Provider specific implementation #####
 
-    async def setup(self):
+    async def setup(self, conf):
         ''' async initialize of module '''
         # start slimproto server
         self.mass.event_loop.create_task(
index cc8c4057e87a75538ad49532f93243d5d19539ac..b0d1d31f641c7d02316d38e1e3ec362e41423bb0 100644 (file)
@@ -24,8 +24,6 @@ CONFIG_ENTRIES = [
     (CONF_ENABLED, True, CONF_ENABLED),
     ]
 
-PLAYER_CONFIG_ENTRIES = []
-
 EVENT_WEBPLAYER_CMD = 'webplayer command'
 EVENT_WEBPLAYER_STATE = 'webplayer state'
 EVENT_WEBPLAYER_REGISTER = 'webplayer register'
@@ -38,15 +36,9 @@ class WebPlayerProvider(PlayerProvider):
         and our internal event bus
     '''
 
-    def __init__(self, mass, conf):
-        super().__init__(mass, conf)
-        self.prov_id = PROV_ID
-        self.name = PROV_NAME
-        self.player_config_entries = PLAYER_CONFIG_ENTRIES
-
      ### Provider specific implementation #####
 
-    async def setup(self):
+    async def setup(self, conf):
         ''' async initialize of module '''
         await self.mass.add_event_listener(self.handle_mass_event, EVENT_WEBPLAYER_STATE)
         await self.mass.add_event_listener(self.handle_mass_event, EVENT_WEBPLAYER_REGISTER)
index 43be7c080c14fa273c91660c1bcd293e249da446..7b420417bdfc2bd3bd2d69663db01d5e058af85d 100755 (executable)
@@ -3,7 +3,6 @@
 
 import asyncio
 import logging
-from concurrent.futures import ThreadPoolExecutor
 import socket
 import importlib
 import os
@@ -15,7 +14,7 @@ except ImportError:
     import json
 LOGGER = logging.getLogger('music_assistant')
 
-from .constants import CONF_KEY_MUSICPROVIDERS, CONF_ENABLED
+from .constants import CONF_KEY_MUSICPROVIDERS, CONF_KEY_PLAYERPROVIDERS, CONF_ENABLED
 
 IS_HASSIO = os.path.isfile('/data/options.json')
 
@@ -40,13 +39,13 @@ def run_background_task(corofn, *args, executor=None):
 def run_async_background_task(executor, corofn, *args):
     ''' run async task in background '''
     def run_task(corofn, *args):
-        LOGGER.debug('running %s in background task' % corofn.__name__)
+        LOGGER.debug('running %s in background task', corofn.__name__)
         new_loop = asyncio.new_event_loop()
         asyncio.set_event_loop(new_loop)
         coro = corofn(*args)
         res = new_loop.run_until_complete(coro)
         new_loop.close()
-        LOGGER.debug('completed %s in background task' % corofn.__name__)
+        LOGGER.debug('completed %s in background task', corofn.__name__)
         return res
     return asyncio.get_event_loop().run_in_executor(executor, run_task, corofn, *args)
 
@@ -70,7 +69,7 @@ async def iter_items(items):
         yield items
     else:
         for item in items:
-            yield items
+            yield item
 
 def try_parse_float(possible_float):
     try:
@@ -204,38 +203,54 @@ def try_load_json_file(jsonfile):
         LOGGER.debug("Could not load json from file %s - %s" % (jsonfile, str(exc)))
         return None
 
-def load_provider_modules(mass, prov_type=CONF_KEY_MUSICPROVIDERS):
+async def load_provider_modules(mass, provider_modules, prov_type=CONF_KEY_MUSICPROVIDERS):
     ''' dynamically load music/player providers '''
-    provider_modules = {}
     base_dir = os.path.dirname(os.path.abspath(__file__))
     modules_path = os.path.join(base_dir, prov_type )
+    # load modules
     for item in os.listdir(modules_path):
         if (os.path.isfile(os.path.join(modules_path, item)) and not item.startswith("_") and 
                 item.endswith('.py') and not item.startswith('.')):
             module_name = item.replace(".py","")
-            prov_mod = load_provider_module(mass, module_name, prov_type)
-            if prov_mod:
-                provider_modules[prov_mod.prov_id] = prov_mod
-    return provider_modules
+            if module_name not in provider_modules:
+                prov_mod = await load_provider_module(mass, module_name, prov_type)
+                if prov_mod:
+                    provider_modules[module_name] = prov_mod
+    # unload modules (if needed)
+    removed_modules = []
+    for prov_id, prov in provider_modules.items():
+        if not mass.config[prov_type][prov_id][CONF_ENABLED]:
+            removed_modules.append(prov_id)
+            if hasattr(prov, 'http_session'):
+                await prov.http_session.close()
+            if prov_type == CONF_KEY_PLAYERPROVIDERS:
+                for player in prov.players:
+                    await mass.players.remove_player(player.player_id)
+    for prov_id in removed_modules:
+        provider_modules.pop(prov_id, None)
+        LOGGER.info('Unloaded %s module', prov_id)
 
-
-def load_provider_module(mass, module_name, prov_type):
+async def load_provider_module(mass, module_name, prov_type):
     ''' dynamically load music/player provider '''
-    LOGGER.debug("Loading provider module %s" % module_name)
     try:
         prov_mod = importlib.import_module(f".{module_name}", 
                 f"music_assistant.{prov_type}")
         prov_conf_entries = prov_mod.CONFIG_ENTRIES
-        prov_id = prov_mod.PROV_ID
+        prov_id = module_name
+        prov_name = prov_mod.PROV_NAME
+        prov_class = prov_mod.PROV_CLASS
         # get/create config for the module
         prov_config = mass.config.create_module_config(
                 prov_id, prov_conf_entries, prov_type)
         if prov_config[CONF_ENABLED]:
-            prov_mod_cls = getattr(prov_mod, prov_mod.PROV_CLASS)
-            provider = prov_mod_cls(mass, prov_config)
-            LOGGER.info("Successfully initialized module %s" % provider.name)
+            prov_mod_cls = getattr(prov_mod, prov_class)
+            provider = prov_mod_cls(mass)
+            provider.prov_id = prov_id
+            provider.name = prov_name
+            await provider.setup(prov_config)
+            LOGGER.info("Successfully initialized module %s", provider.name)
             return provider
         else:
             return None
     except Exception as exc:
-        LOGGER.exception("Error loading module %s: %s" %(module_name, exc))
+        LOGGER.exception("Error loading module %s: %s", module_name, exc)
index ffe27f01dd0911c191c5d62bbd36daec5f66b6df..2a50babe311cda2fe1f6bc2aa8ecf5dd05963e1d 100755 (executable)
@@ -13,75 +13,81 @@ import concurrent
 import threading
 from .models.media_types import MediaItem, MediaType, media_type_from_string
 from .utils import run_periodic, LOGGER, IS_HASSIO, run_async_background_task, get_ip, json_serializer
+from .constants import CONF_KEY_PLAYERSETTINGS, CONF_KEY_MUSICPROVIDERS, CONF_KEY_PLAYERPROVIDERS
 
 CONF_KEY = 'web'
 
 if IS_HASSIO:
     # on hassio we use ingress
-    CONFIG_ENTRIES = []
+    CONFIG_ENTRIES = [('https_port', 8096, 'web_https_port'),
+                      ('ssl_certificate', '', 'web_ssl_cert'),
+                      ('ssl_key', '', 'web_ssl_key'),
+                      ('cert_fqdn_host', '', 'cert_fqdn_host')]
 else:
-    CONFIG_ENTRIES = [
-            ('http_port', 8095, 'web_http_port'),
-            ('https_port', 8096, 'web_https_port'),
-            ('ssl_certificate', '', 'web_ssl_cert'), 
-            ('ssl_key', '', 'web_ssl_key'),
-            ('cert_fqdn_host', '', 'cert_fqdn_host')
-            ]
+    CONFIG_ENTRIES = [('http_port', 8095, 'web_http_port'),
+                      ('https_port', 8096, 'web_https_port'),
+                      ('ssl_certificate', '', 'web_ssl_cert'),
+                      ('ssl_key', '', 'web_ssl_key'),
+                      ('cert_fqdn_host', '', 'cert_fqdn_host')]
+
 
 class ClassRouteTableDef(web.RouteTableDef):
     def __repr__(self) -> str:
         return "<ClassRouteTableDef count={}>".format(len(self._items))
 
-    def route(self,
-              method: str,
-              path: str,
-              **kwargs):
+    def route(self, method: str, path: str, **kwargs):
         def inner(handler):
             handler.route_info = (method, path, kwargs)
             return handler
+
         return inner
 
     def add_class_routes(self, instance) -> None:
         def predicate(member) -> bool:
             return all((inspect.iscoroutinefunction(member),
                         hasattr(member, "route_info")))
+
         for _, handler in inspect.getmembers(instance, predicate):
             method, path, kwargs = handler.route_info
             super().route(method, path, **kwargs)(handler)
+
+
 routes = ClassRouteTableDef()
 
+
 class Web():
     """ webserver and json/websocket api """
     runner = None
-    
+
     def __init__(self, mass):
         self.mass = mass
         # load/create/update config
-        config = self.mass.config.create_module_config(CONF_KEY, CONFIG_ENTRIES)
+        config = self.mass.config.create_module_config(CONF_KEY,
+                                                       CONFIG_ENTRIES)
         self.local_ip = get_ip()
         self.config = config
         if IS_HASSIO:
-            # retrieve ingress port
+            # retrieve ingress http port
             import requests
-            response = requests.get(
-                    "http://hassio/addons/self/info", 
-                    headers = {"X-HASSIO-KEY": os.environ["HASSIO_TOKEN"]}).json()
+            url = 'http://hassio/addons/self/info'
+            headers = { "X-HASSIO-KEY":os.environ["HASSIO_TOKEN"] }
+            response = requests.get(url, headers=headers).json()
             self.http_port = response["data"]["ingress_port"]
-            self.https_port = 0
-            self._enable_ssl = False
         else:
             # use settings from config
             self.http_port = config['http_port']
-            enable_ssl = config['ssl_certificate'] and config['ssl_key']
-            if config['ssl_certificate'] and not os.path.isfile(
-                    config['ssl_certificate']):
-                enable_ssl = False
-                LOGGER.warning("SSL certificate file not found: %s", config['ssl_certificate'])
-            if config['ssl_key'] and not os.path.isfile(config['ssl_key']):
-                enable_ssl = False
-                LOGGER.warning( "SSL certificate key file not found: %s", config['ssl_key'])
-            self.https_port = config['https_port']
-            self._enable_ssl = enable_ssl
+        enable_ssl = config['ssl_certificate'] and config['ssl_key']
+        if config['ssl_certificate'] and not os.path.isfile(
+                config['ssl_certificate']):
+            enable_ssl = False
+            LOGGER.warning("SSL certificate file not found: %s",
+                           config['ssl_certificate'])
+        if config['ssl_key'] and not os.path.isfile(config['ssl_key']):
+            enable_ssl = False
+            LOGGER.warning("SSL certificate key file not found: %s",
+                           config['ssl_key'])
+        self.https_port = config['https_port']
+        self._enable_ssl = enable_ssl
 
     async def setup(self):
         """ perform async setup """
@@ -89,20 +95,27 @@ class Web():
         app = web.Application()
         app.add_routes(routes)
         app.add_routes([
-            web.get('/stream/{player_id}', self.mass.http_streamer.stream, allow_head=False),
-            web.get('/stream/{player_id}/{queue_item_id}', self.mass.http_streamer.stream, allow_head=False),
+            web.get('/stream/{player_id}',
+                    self.mass.http_streamer.stream,
+                    allow_head=False),
+            web.get('/stream/{player_id}/{queue_item_id}',
+                    self.mass.http_streamer.stream,
+                    allow_head=False),
             web.get('/', self.index),
             web.get('/jsonrpc.js', self.json_rpc),
             web.post('/jsonrpc.js', self.json_rpc),
             web.get('/ws', self.websocket_handler)
         ])
-        webdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web/')
+        webdir = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+                              'web/')
         app.router.add_static("/", webdir)
-        
+
         # Add CORS support to all routes
-        cors = aiohttp_cors.setup( app,
+        cors = aiohttp_cors.setup(
+            app,
             defaults={
-                "*": aiohttp_cors.ResourceOptions(
+                "*":
+                aiohttp_cors.ResourceOptions(
                     allow_credentials=True,
                     expose_headers="*",
                     allow_headers="*",
@@ -117,14 +130,19 @@ class Web():
         LOGGER.info("Started HTTP webserver on port %s", self.http_port)
         if self._enable_ssl:
             ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
-            ssl_context.load_cert_chain(self.config['ssl_certificate'], self.config['ssl_key'])
-            https_site = web.TCPSite(self.runner, '0.0.0.0', self.config['https_port'], ssl_context=ssl_context)
+            ssl_context.load_cert_chain(self.config['ssl_certificate'],
+                                        self.config['ssl_key'])
+            https_site = web.TCPSite(self.runner,
+                                     '0.0.0.0',
+                                     self.config['https_port'],
+                                     ssl_context=ssl_context)
             await https_site.start()
-            LOGGER.info("Started HTTPS webserver on port %s", self.config['https_port'])
+            LOGGER.info("Started HTTPS webserver on port %s",
+                        self.config['https_port'])
 
     async def index(self, request):
-        index_file = os.path.join(
-                os.path.dirname(os.path.abspath(__file__)), 'web/index.html')
+        index_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+                                  'web/index.html')
         return web.FileResponse(index_file)
 
     @routes.get('/api/library/artists')
@@ -132,7 +150,8 @@ class Web():
         """Get all library artists."""
         orderby = request.query.get('orderby', 'name')
         provider_filter = request.rel_url.query.get('provider')
-        iterator = self.mass.music.library_artists(orderby=orderby, provider_filter=provider_filter)
+        iterator = self.mass.music.library_artists(
+            orderby=orderby, provider_filter=provider_filter)
         return await self.__stream_json(request, iterator)
 
     @routes.get('/api/library/albums')
@@ -140,7 +159,8 @@ class Web():
         """Get all library albums."""
         orderby = request.query.get('orderby', 'name')
         provider_filter = request.rel_url.query.get('provider')
-        iterator = self.mass.music.library_albums(orderby=orderby, provider_filter=provider_filter)
+        iterator = self.mass.music.library_albums(
+            orderby=orderby, provider_filter=provider_filter)
         return await self.__stream_json(request, iterator)
 
     @routes.get('/api/library/tracks')
@@ -148,7 +168,8 @@ class Web():
         """Get all library tracks."""
         orderby = request.query.get('orderby', 'name')
         provider_filter = request.rel_url.query.get('provider')
-        iterator = self.mass.music.library_tracks(orderby=orderby, provider_filter=provider_filter)
+        iterator = self.mass.music.library_tracks(
+            orderby=orderby, provider_filter=provider_filter)
         return await self.__stream_json(request, iterator)
 
     @routes.get('/api/library/radios')
@@ -156,7 +177,8 @@ class Web():
         """Get all library radios."""
         orderby = request.query.get('orderby', 'name')
         provider_filter = request.rel_url.query.get('provider')
-        iterator = self.mass.music.library_radios(orderby=orderby, provider_filter=provider_filter)
+        iterator = self.mass.music.library_radios(
+            orderby=orderby, provider_filter=provider_filter)
         return await self.__stream_json(request, iterator)
 
     @routes.get('/api/library/playlists')
@@ -164,7 +186,8 @@ class Web():
         """Get all library playlists."""
         orderby = request.query.get('orderby', 'name')
         provider_filter = request.rel_url.query.get('provider')
-        iterator = self.mass.music.library_playlists(orderby=orderby, provider_filter=provider_filter)
+        iterator = self.mass.music.library_playlists(
+            orderby=orderby, provider_filter=provider_filter)
         return await self.__stream_json(request, iterator)
 
     @routes.put('/api/library')
@@ -193,7 +216,7 @@ class Web():
             return web.Response(text='invalid item or provider', status=501)
         result = await self.mass.music.artist(item_id, provider, lazy=lazy)
         return web.json_response(result, dumps=json_serializer)
-    
+
     @routes.get('/api/albums/{item_id}')
     async def album(self, request):
         """ get full album details"""
@@ -244,13 +267,17 @@ class Web():
         media_id = request.match_info.get('media_id')
         provider = request.rel_url.query.get('provider')
         if (media_id is None or provider is None):
-            return web.Response(text='invalid media_id or provider', status=501)
+            return web.Response(text='invalid media_id or provider',
+                                status=501)
         size = int(request.rel_url.query.get('size', 0))
         img_file = await self.mass.music.get_image_thumb(
-                    media_id, media_type, provider, size)
+            media_id, media_type, provider, size)
         if not img_file or not os.path.isfile(img_file):
             return web.Response(status=404)
-        headers = {'Cache-Control': 'max-age=86400, public', 'Pragma': 'public'}
+        headers = {
+            'Cache-Control': 'max-age=86400, public',
+            'Pragma': 'public'
+        }
         return web.FileResponse(img_file, headers=headers)
 
     @routes.get('/api/artists/{item_id}/toptracks')
@@ -330,7 +357,10 @@ class Web():
         if not media_types_query or "radios" in media_types_query:
             media_types.append(MediaType.Radio)
         # get results from database
-        result = await self.mass.music.search(searchquery, media_types, limit=limit, online=online)
+        result = await self.mass.music.search(searchquery,
+                                              media_types,
+                                              limit=limit,
+                                              online=online)
         return web.json_response(result, dumps=json_serializer)
 
     @routes.get('/api/players')
@@ -357,8 +387,8 @@ class Web():
             result = await player_cmd()
         else:
             return web.Response(text='invalid command', status=501)
-        return web.json_response(result, dumps=json_serializer) 
-    
+        return web.json_response(result, dumps=json_serializer)
+
     @routes.post('/api/players/{player_id}/play_media/{queue_opt}')
     async def player_play_media(self, request):
         """ issue player play_media command"""
@@ -369,9 +399,10 @@ class Web():
         queue_opt = request.match_info.get('queue_opt', 'play')
         body = await request.json()
         media_items = await self.__media_items_from_body(body)
-        result = await self.mass.players.play_media(player_id, media_items, queue_opt)
+        result = await self.mass.players.play_media(player_id, media_items,
+                                                    queue_opt)
         return web.json_response(result, dumps=json_serializer)
-    
+
     @routes.get('/api/players/{player_id}/queue/items/{queue_item}')
     async def player_queue_item(self, request):
         """ return item (by index or queue item id) from the player's queue """
@@ -384,17 +415,19 @@ class Web():
         except ValueError:
             queue_item = await player.queue.by_item_id(item_id)
         return web.json_response(queue_item, dumps=json_serializer)
-    
+
     @routes.get('/api/players/{player_id}/queue/items')
     async def player_queue_items(self, request):
         """ return the items in the player's queue """
         player_id = request.match_info.get('player_id')
         player = await self.mass.players.get_player(player_id)
+
         async def queue_tracks_iter():
             for item in player.queue.items:
                 yield item
+
         return await self.__stream_json(request, queue_tracks_iter())
-    
+
     @routes.get('/api/players/{player_id}/queue')
     async def player_queue(self, request):
         """ return the player queue details """
@@ -445,19 +478,33 @@ class Web():
         conf_key = request.match_info.get('key')
         conf_subkey = request.match_info.get('subkey')
         new_values = await request.json()
-        LOGGER.debug(f'save config called for {conf_key}/{conf_subkey} - new value: {new_values}')
+        LOGGER.debug(
+            f'save config called for {conf_key}/{conf_subkey} - new value: {new_values}'
+        )
         cur_values = self.mass.config[conf_key][conf_subkey]
-        result = {"success": True, "restart_required": False, "settings_changed": False}
+        result = {
+            "success": True,
+            "restart_required": False,
+            "settings_changed": False
+        }
         if cur_values != new_values:
             # config changed
             result["settings_changed"] = True
             self.mass.config[conf_key][conf_subkey] = new_values
-            if conf_key == "player_settings":
-                # player settings don't require restart, force update of player
+            if conf_key == CONF_KEY_PLAYERSETTINGS:
+                # player settings: force update of player
                 self.mass.event_loop.create_task(
                     self.mass.players.trigger_update(conf_subkey))
+            elif conf_key == CONF_KEY_MUSICPROVIDERS:
+                # (re)load music provider modules
+                self.mass.event_loop.create_task(
+                    self.mass.music.load_modules())
+            elif conf_key == CONF_KEY_PLAYERPROVIDERS:
+                # (re)load player provider modules
+                self.mass.event_loop.create_task(
+                    self.mass.players.load_modules())
             else:
-                # TODO: allow some settings without restart ?
+                # other settings need restart
                 result["restart_required"] = True
             self.mass.config.save()
         return web.json_response(result)
@@ -469,26 +516,29 @@ class Web():
         try:
             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 }
+                ws_msg = {"message": msg, "message_details": msg_details}
                 try:
                     await ws.send_json(ws_msg)
                 except (AssertionError, asyncio.CancelledError):
                     await self.mass.remove_event_listener(cb_id)
+
             cb_id = await self.mass.add_event_listener(send_event)
             # process incoming messages
             async for msg in ws:
                 if msg.type == aiohttp.WSMsgType.ERROR:
                     LOGGER.debug('ws connection closed with exception %s' %
-                        ws.exception())
+                                 ws.exception())
                 elif msg.type != aiohttp.WSMsgType.TEXT:
                     LOGGER.warning(msg.data)
                 else:
                     data = msg.json()
                     # echo the websocket message on event bus
                     # can be picked up by other modules, e.g. the webplayer
-                    await self.mass.signal_event(data['message'], data['message_details'])
+                    await self.mass.signal_event(data['message'],
+                                                 data['message_details'])
         except (Exception, AssertionError, asyncio.CancelledError) as exc:
             LOGGER.warning("Websocket disconnected - %s" % str(exc))
         finally:
@@ -549,18 +599,20 @@ class Web():
         else:
             return web.Response(text='command not supported')
         return web.Response(text='success')
-    
+
     async def __media_items_from_body(self, data):
         """Helper to turn posted body data into media items."""
         if not isinstance(data, list):
             data = [data]
         media_items = []
         for item in data:
-            media_item = await self.mass.music.item(
-                item['item_id'], item['media_type'], item['provider'], lazy=True)
+            media_item = await self.mass.music.item(item['item_id'],
+                                                    item['media_type'],
+                                                    item['provider'],
+                                                    lazy=True)
             media_items.append(media_item)
         return media_items
-    
+
     async def __stream_json(self, request, iterator):
         """ stream items from async iterator as json object """
         resp = web.StreamResponse(status=200,