From b6628a6e59b83e0fcfc7a56ed7e11a86f18861b0 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 28 Oct 2023 00:20:43 +0200 Subject: [PATCH] A few small fixes (#901) --- music_assistant/client/music.py | 2 +- .../common/models/config_entries.py | 3 +- music_assistant/common/models/provider.py | 2 +- music_assistant/server/controllers/config.py | 28 +++++++++++++++++++ music_assistant/server/controllers/music.py | 24 ++++++++-------- music_assistant/server/controllers/players.py | 5 ++-- music_assistant/server/server.py | 7 ++++- 7 files changed, 54 insertions(+), 17 deletions(-) diff --git a/music_assistant/client/music.py b/music_assistant/client/music.py index b70dcf79..21618865 100644 --- a/music_assistant/client/music.py +++ b/music_assistant/client/music.py @@ -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, ) diff --git a/music_assistant/common/models/config_entries.py b/music_assistant/common/models/config_entries.py index ae2c673a..5824b662 100644 --- a/music_assistant/common/models/config_entries.py +++ b/music_assistant/common/models/config_entries.py @@ -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 diff --git a/music_assistant/common/models/provider.py b/music_assistant/common/models/provider.py index b953ee3e..6370e502 100644 --- a/music_assistant/common/models/provider.py +++ b/music_assistant/common/models/provider.py @@ -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 diff --git a/music_assistant/server/controllers/config.py b/music_assistant/server/controllers/config.py index a08784ef..feefaa4c 100644 --- a/music_assistant/server/controllers/config.py +++ b/music_assistant/server/controllers/config.py @@ -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}" diff --git a/music_assistant/server/controllers/music.py b/music_assistant/server/controllers/music.py index 3d0e0926..791deb86 100755 --- a/music_assistant/server/controllers/music.py +++ b/music_assistant/server/controllers/music.py @@ -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") diff --git a/music_assistant/server/controllers/players.py b/music_assistant/server/controllers/players.py index f35ec882..62f311de 100755 --- a/music_assistant/server/controllers/players.py +++ b/music_assistant/server/controllers/players.py @@ -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) diff --git a/music_assistant/server/server.py b/music_assistant/server/server.py index 2bd1aa33..e6b8c7d4 100644 --- a/music_assistant/server/server.py +++ b/music_assistant/server/server.py @@ -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.""" -- 2.34.1