A few small fixes (#901)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 27 Oct 2023 22:20:43 +0000 (00:20 +0200)
committerGitHub <noreply@github.com>
Fri, 27 Oct 2023 22:20:43 +0000 (00:20 +0200)
music_assistant/client/music.py
music_assistant/common/models/config_entries.py
music_assistant/common/models/provider.py
music_assistant/server/controllers/config.py
music_assistant/server/controllers/music.py
music_assistant/server/controllers/players.py
music_assistant/server/server.py

index b70dcf79bcc0b470cc277153a91569cdd7a455c8..21618865e1b68852eafe3ba371892e039a345654 100644 (file)
@@ -153,7 +153,7 @@ class Music:
         return [
             Track.from_dict(item)
             for item in await self.client.send_command(
-                "music/album/tracks",
+                "music/album/album_tracks",
                 item_id=item_id,
                 provider_instance_id_or_domain=provider_instance_id_or_domain,
             )
index ae2c673ac5176864934b62994bcde42dbf68149e..5824b6622d8bb7c7569156b3dcc6d1e71c0190ad 100644 (file)
@@ -216,7 +216,8 @@ class Config(DataClassDictMixin):
             changed_keys.add(key)
 
         # config entry values
-        for key, new_val in update.items():
+        values = update.get("values", update)
+        for key, new_val in values.items():
             if key in root_values:
                 continue
             cur_val = self.values[key].value if key in self.values else None
index b953ee3e536c60d8d7fed4dc1b0b07f8d046968c..6370e502a88338fa5b2925b4cbfef98b5b50ab1b 100644 (file)
@@ -33,7 +33,7 @@ class ProviderManifest(DataClassORJSONMixin):
     builtin: bool = False
     # hidden: hide entry in the UI
     hidden: bool = False
-    # load_by_default: load this provider by default (mostly used together with `builtin`)
+    # load_by_default: load this provider by default (may be used together with `builtin`)
     load_by_default: bool = False
     # depends_on: depends on another provider to function
     depends_on: str | None = None
index a08784efc4ab0ea09461f2bc2368e968ce1a31a6..feefaa4cdeb92cb7163b08d81fb230d9be586572 100644 (file)
@@ -264,11 +264,28 @@ class ConfigController:
         existing = self.get(conf_key)
         if not existing:
             raise KeyError(f"Provider {instance_id} does not exist")
+        prov_manifest = self.mass.get_provider_manifest(existing["domain"])
+        if prov_manifest.load_by_default and instance_id == prov_manifest.domain:
+            # Guard for a provider that is loaded by default
+            LOGGER.warning(
+                "Provider %s can not be removed, disabling instead...", prov_manifest.name
+            )
+            existing["enabled"] = False
+            await self._update_provider_config(instance_id, existing)
+            return
+        if prov_manifest.builtin:
+            raise RuntimeError(f"Builtin provider {prov_manifest.name} can not be removed.")
         self.remove(conf_key)
         await self.mass.unload_provider(instance_id)
         if existing["type"] == "music":
             # cleanup entries in library
             await self.mass.music.cleanup_provider(instance_id)
+        if existing["type"] == "player":
+            # cleanup entries in player manager
+            for player in self.mass.players:
+                if player.provider != instance_id:
+                    continue
+                self.mass.players.remove(player.player_id, cleanup_config=True)
 
     async def remove_provider_config_value(self, instance_id: str, key: str) -> None:
         """Remove/reset single Provider config value."""
@@ -619,11 +636,22 @@ class ConfigController:
             await self._load_provider_config(config)
         else:
             # disable provider
+            prov_manifest = self.mass.get_provider_manifest(config.domain)
+            if prov_manifest.builtin:
+                raise RuntimeError("Builtin provider can not be disabled.")
             # also unload any other providers dependent of this provider
             for dep_prov in self.mass.providers:
                 if dep_prov.manifest.depends_on == config.domain:
                     await self.mass.unload_provider(dep_prov.instance_id)
             await self.mass.unload_provider(config.instance_id)
+            if config.type == ProviderType.PLAYER:
+                # cleanup entries in player manager
+                for player in self.mass.players.all(
+                    return_unavailable=True, return_hidden=True, return_disabled=True
+                ):
+                    if player.provider != instance_id:
+                        continue
+                    self.mass.players.remove(player.player_id, cleanup_config=False)
         # load succeeded, save new config
         config.last_error = None
         conf_key = f"{CONF_PROVIDERS}/{instance_id}"
index 3d0e0926412db843698ef9637c65c3f770309db8..791deb86531f76b4d083d75280734fb305ceb1e7 100755 (executable)
@@ -60,6 +60,7 @@ class MusicController(CoreController):
 
     domain: str = "music"
     database: DatabaseConnection | None = None
+    config: CoreConfig
 
     def __init__(self, *args, **kwargs) -> None:
         """Initialize class."""
@@ -99,11 +100,12 @@ class MusicController(CoreController):
 
     async def setup(self, config: CoreConfig) -> None:
         """Async initialize of module."""
+        self.config = config
         # setup library database
         await self._setup_database()
         sync_interval = config.get_value(CONF_SYNC_INTERVAL)
-        self.logger.info("Setting up the sync interval to %s minutes.", sync_interval)
-        self._sync_task = self.mass.create_task(self.start_sync(reschedule=sync_interval))
+        self.logger.info("Using a sync interval of %s minutes.", sync_interval)
+        self._schedule_sync()
 
     async def close(self) -> None:
         """Cleanup on exit."""
@@ -117,11 +119,10 @@ class MusicController(CoreController):
         return self.mass.get_providers(ProviderType.MUSIC)
 
     @api_command("music/sync")
-    async def start_sync(
+    def start_sync(
         self,
         media_types: list[MediaType] | None = None,
         providers: list[str] | None = None,
-        reschedule: int | None = None,
     ) -> None:
         """Start running the sync of (all or selected) musicproviders.
 
@@ -138,13 +139,6 @@ class MusicController(CoreController):
                 continue
             self._start_provider_sync(provider.instance_id, media_types)
 
-        # reschedule task if needed
-        def create_sync_task():
-            self.mass.create_task(self.start_sync, media_types, providers, reschedule)
-
-        if reschedule is not None:
-            self.mass.loop.call_later(reschedule, create_sync_task)
-
     @api_command("music/synctasks")
     def get_running_sync_tasks(self) -> list[SyncTask]:
         """Return list with providers that are currently (scheduled for) syncing."""
@@ -620,6 +614,14 @@ class MusicController(CoreController):
             for item in prov_items:
                 await ctrl.remove_provider_mappings(item.item_id, provider_instance)
 
+    def _schedule_sync(self) -> None:
+        """Schedule the periodic sync."""
+        self.start_sync()
+        sync_interval = self.config.get_value(CONF_SYNC_INTERVAL)
+        # we reschedule ourselves right after execution
+        # NOTE: sync_interval is stored in minutes, we need seconds
+        self.mass.loop.call_later(sync_interval * 60, self._schedule_sync)
+
     async def _setup_database(self):
         """Initialize database."""
         db_path = os.path.join(self.mass.storage_path, "library.db")
index f35ec882406b36b0fe6c7e44ca01262065fdfc4b..62f311ded2de0092c1b821d80c563f70b1cd6143 100755 (executable)
@@ -210,14 +210,15 @@ class PlayerController(CoreController):
         self.register(player)
 
     @api_command("players/remove")
-    def remove(self, player_id: str) -> None:
+    def remove(self, player_id: str, cleanup_config: bool = True) -> None:
         """Remove a player from the registry."""
         player = self._players.pop(player_id, None)
         if player is None:
             return
         LOGGER.info("Player removed: %s", player.name)
         self.mass.player_queues.on_player_remove(player_id)
-        self.mass.config.remove(f"players/{player_id}")
+        if cleanup_config:
+            self.mass.config.remove(f"players/{player_id}")
         self._prev_states.pop(player_id, None)
         self.mass.signal_event(EventType.PLAYER_REMOVED, player_id)
 
index 2bd1aa3305daea750098b56cfbef1b4fc486008d..e6b8c7d4137a1c21fcfa62a4e106da399525da55 100644 (file)
@@ -198,6 +198,11 @@ class MusicAssistant:
         """Return all Provider manifests."""
         return list(self._provider_manifests.values())
 
+    @api_command("providers/manifests/get")
+    def get_provider_manifest(self, domain: str) -> ProviderManifest:
+        """Return Provider manifests of single provider(domain)."""
+        return self._provider_manifests[domain]
+
     @api_command("providers")
     def get_providers(
         self, provider_type: ProviderType | None = None
@@ -410,7 +415,7 @@ class MusicAssistant:
         self.signal_event(EventType.PROVIDERS_UPDATED, data=self.get_providers())
         # if this is a music provider, start sync
         if provider.type == ProviderType.MUSIC:
-            await self.music.start_sync(providers=[provider.instance_id])
+            self.music.start_sync(providers=[provider.instance_id])
 
     async def unload_provider(self, instance_id: str) -> None:
         """Unload a provider."""