From d9bd398513c2a10e945c3fac6848eb2a3e225a3c Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 14 Mar 2023 23:59:31 +0100 Subject: [PATCH] Some small bugfixes and improvements (#532) - [add some guards for missing metadata in cast](https://github.com/music-assistant/server/commit/993e17d1f905bdbd21912df84fc9ffa0166e9341) - [only restart airplay bridge if needed](https://github.com/music-assistant/server/commit/f2b038896a6cfc5b980400cd8465e629a72b4938) - [add more guards for unavailable providers](https://github.com/music-assistant/server/commit/452f52c8c7bd15b330a095ccc10da3bfd1ac96c0) --- .../common/models/config_entries.py | 2 +- .../server/controllers/media/albums.py | 19 +++++++++---- .../server/controllers/media/artists.py | 23 ++++++++++----- .../server/controllers/media/base.py | 14 +++++++--- .../server/controllers/media/tracks.py | 13 +++++++-- music_assistant/server/controllers/music.py | 7 +++-- .../server/providers/airplay/__init__.py | 28 +++++++++++++------ .../server/providers/chromecast/__init__.py | 8 ++++-- 8 files changed, 81 insertions(+), 33 deletions(-) diff --git a/music_assistant/common/models/config_entries.py b/music_assistant/common/models/config_entries.py index 694de33c..b5e9f543 100644 --- a/music_assistant/common/models/config_entries.py +++ b/music_assistant/common/models/config_entries.py @@ -202,7 +202,7 @@ class Config(DataClassDictMixin): if cur_val == new_val: continue self.values[key].value = new_val - changed_keys.add(f"values.{key}") + changed_keys.add(f"values/{key}") return changed_keys diff --git a/music_assistant/server/controllers/media/albums.py b/music_assistant/server/controllers/media/albums.py index 35f4520a..22fc4ee6 100644 --- a/music_assistant/server/controllers/media/albums.py +++ b/music_assistant/server/controllers/media/albums.py @@ -8,7 +8,11 @@ from typing import TYPE_CHECKING from music_assistant.common.helpers.json import serialize_to_json from music_assistant.common.models.enums import EventType, ProviderFeature -from music_assistant.common.models.errors import MediaNotFoundError, UnsupportedFeaturedException +from music_assistant.common.models.errors import ( + MediaNotFoundError, + ProviderUnavailableError, + UnsupportedFeaturedException, +) from music_assistant.common.models.media_items import ( Album, AlbumType, @@ -268,9 +272,11 @@ class AlbumsController(MediaControllerBase[Album]): provider_instance: str | None = None, ) -> list[Track]: """Return album tracks for the given provider album id.""" - prov = self.mass.get_provider(provider_instance or provider_domain) - if not prov: + try: + prov = self.mass.get_provider(provider_instance or provider_domain) + except ProviderUnavailableError: return [] + full_album = await self.get_provider_item(item_id, provider_instance or provider_domain) # prefer cache items (if any) cache_key = f"{prov.instance_id}.albumtracks.{item_id}" @@ -299,8 +305,11 @@ class AlbumsController(MediaControllerBase[Album]): limit: int = 25, ): """Generate a dynamic list of tracks based on the album content.""" - prov = self.mass.get_provider(provider_instance or provider_domain) - if not prov or ProviderFeature.SIMILAR_TRACKS not in prov.supported_features: + try: + prov = self.mass.get_provider(provider_instance or provider_domain) + except ProviderUnavailableError: + return [] + if ProviderFeature.SIMILAR_TRACKS not in prov.supported_features: return [] album_tracks = await self._get_provider_album_tracks( item_id=item_id, diff --git a/music_assistant/server/controllers/media/artists.py b/music_assistant/server/controllers/media/artists.py index 000f857b..a81acd8a 100644 --- a/music_assistant/server/controllers/media/artists.py +++ b/music_assistant/server/controllers/media/artists.py @@ -10,7 +10,11 @@ from typing import TYPE_CHECKING, Any from music_assistant.common.helpers.json import serialize_to_json from music_assistant.common.models.enums import EventType, ProviderFeature -from music_assistant.common.models.errors import MediaNotFoundError, UnsupportedFeaturedException +from music_assistant.common.models.errors import ( + MediaNotFoundError, + ProviderUnavailableError, + UnsupportedFeaturedException, +) from music_assistant.common.models.media_items import ( Album, AlbumType, @@ -182,8 +186,9 @@ class ArtistsController(MediaControllerBase[Artist]): cache_checksum: Any = None, ) -> list[Track]: """Return top tracks for an artist on given provider.""" - prov = self.mass.get_provider(provider_instance or provider_domain) - if not prov: + try: + prov = self.mass.get_provider(provider_instance or provider_domain) + except ProviderUnavailableError: return [] # prefer cache items (if any) cache_key = f"{prov.instance_id}.artist_toptracks.{item_id}" @@ -219,8 +224,9 @@ class ArtistsController(MediaControllerBase[Artist]): cache_checksum: Any = None, ) -> list[Album]: """Return albums for an artist on given provider.""" - prov = self.mass.get_provider(provider_instance or provider_domain) - if not prov: + try: + prov = self.mass.get_provider(provider_instance or provider_domain) + except ProviderUnavailableError: return [] # prefer cache items (if any) cache_key = f"{prov.instance_id}.artist_albums.{item_id}" @@ -365,8 +371,11 @@ class ArtistsController(MediaControllerBase[Artist]): limit: int = 25, ): """Generate a dynamic list of tracks based on the artist's top tracks.""" - prov = self.mass.get_provider(provider_instance or provider_domain) - if not prov or ProviderFeature.SIMILAR_TRACKS not in prov.supported_features: + try: + prov = self.mass.get_provider(provider_instance or provider_domain) + except ProviderUnavailableError: + return [] + if ProviderFeature.SIMILAR_TRACKS not in prov.supported_features: return [] top_tracks = await self.get_provider_artist_toptracks( item_id=item_id, diff --git a/music_assistant/server/controllers/media/base.py b/music_assistant/server/controllers/media/base.py index 3dd67d16..6464fd27 100644 --- a/music_assistant/server/controllers/media/base.py +++ b/music_assistant/server/controllers/media/base.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Generic, TypeVar from music_assistant.common.helpers.json import serialize_to_json from music_assistant.common.models.enums import EventType, MediaType, ProviderFeature -from music_assistant.common.models.errors import MediaNotFoundError +from music_assistant.common.models.errors import MediaNotFoundError, ProviderUnavailableError from music_assistant.common.models.media_items import ( MediaItemType, PagedItems, @@ -193,8 +193,11 @@ class MediaControllerBase(Generic[ItemCls], metaclass=ABCMeta): for db_row in await self.mass.music.database.search(self.db_table, search_query) ] - prov = self.mass.get_provider(provider_instance or provider_domain) - if not prov or ProviderFeature.SEARCH not in prov.supported_features: + try: + prov = self.mass.get_provider(provider_instance or provider_domain) + except ProviderUnavailableError: + return [] + if ProviderFeature.SEARCH not in prov.supported_features: return [] if not prov.library_supported(self.media_type): # assume library supported also means that this mediatype is supported @@ -476,7 +479,10 @@ class MediaControllerBase(Generic[ItemCls], metaclass=ABCMeta): """Return a dynamic list of tracks based on the given item.""" ref_item = await self.get(item_id, provider_domain, provider_instance) for prov_mapping in ref_item.provider_mappings: - prov = self.mass.get_provider(prov_mapping.provider_instance) + try: + prov = self.mass.get_provider(prov_mapping.provider_instance) + except ProviderUnavailableError: + continue if not prov.available: continue if ProviderFeature.SIMILAR_TRACKS not in prov.supported_features: diff --git a/music_assistant/server/controllers/media/tracks.py b/music_assistant/server/controllers/media/tracks.py index ad0c3937..46c867d5 100644 --- a/music_assistant/server/controllers/media/tracks.py +++ b/music_assistant/server/controllers/media/tracks.py @@ -5,7 +5,11 @@ import asyncio from music_assistant.common.helpers.json import serialize_to_json from music_assistant.common.models.enums import EventType, MediaType, ProviderFeature -from music_assistant.common.models.errors import MediaNotFoundError, UnsupportedFeaturedException +from music_assistant.common.models.errors import ( + MediaNotFoundError, + ProviderUnavailableError, + UnsupportedFeaturedException, +) from music_assistant.common.models.media_items import ( Album, Artist, @@ -196,8 +200,11 @@ class TracksController(MediaControllerBase[Track]): limit: int = 25, ): """Generate a dynamic list of tracks based on the track.""" - prov = self.mass.get_provider(provider_instance or provider_domain) - if not prov or ProviderFeature.SIMILAR_TRACKS not in prov.supported_features: + try: + prov = self.mass.get_provider(provider_instance or provider_domain) + except ProviderUnavailableError: + return [] + if ProviderFeature.SIMILAR_TRACKS not in prov.supported_features: return [] # Grab similar tracks from the music provider similar_tracks = await prov.get_similar_tracks(prov_track_id=item_id, limit=limit) diff --git a/music_assistant/server/controllers/music.py b/music_assistant/server/controllers/music.py index d0321e74..ca112e42 100755 --- a/music_assistant/server/controllers/music.py +++ b/music_assistant/server/controllers/music.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING from music_assistant.common.helpers.datetime import utc_timestamp from music_assistant.common.helpers.uri import parse_uri from music_assistant.common.models.enums import EventType, MediaType, ProviderFeature, ProviderType -from music_assistant.common.models.errors import MusicAssistantError +from music_assistant.common.models.errors import MusicAssistantError, ProviderUnavailableError from music_assistant.common.models.media_items import ( BrowseFolder, MediaItem, @@ -154,7 +154,10 @@ class MusicController: :param limit: number of items to return in the search (per type). """ assert provider_domain or provider_instance, "Provider needs to be supplied" - prov = self.mass.get_provider(provider_instance or provider_domain) + try: + prov = self.mass.get_provider(provider_instance or provider_domain) + except ProviderUnavailableError: + return [] if ProviderFeature.SEARCH not in prov.supported_features: return [] diff --git a/music_assistant/server/providers/airplay/__init__.py b/music_assistant/server/providers/airplay/__init__.py index da4f76e6..3d434403 100644 --- a/music_assistant/server/providers/airplay/__init__.py +++ b/music_assistant/server/providers/airplay/__init__.py @@ -67,12 +67,15 @@ PLAYER_CONFIG_ENTRIES = ( ), ) +NEED_BRIDGE_RESTART = {"values/read_ahead", "values/encryption", "values/alac_encode"} + class AirplayProvider(PlayerProvider): """Player provider for Airplay based players, using the slimproto bridge.""" _bridge_bin: str | None = None _bridge_proc: asyncio.subprocess.Process | None = None + _timer_handle: asyncio.TimerHandle | None = None _closing: bool = False _config_file: str | None = None @@ -113,10 +116,8 @@ class AirplayProvider(PlayerProvider): async def update_config(): # stop bridge (it will be auto restarted) - # TODO: only restart bridge if actual xml values changed - await self._stop_bridge() - # update the config - await self._check_config_xml() + if changed_keys.intersection(NEED_BRIDGE_RESTART): + self.restart_bridge() asyncio.create_task(update_config()) @@ -269,7 +270,6 @@ class AirplayProvider(PlayerProvider): async def _bridge_process_runner(self) -> None: """Run the bridge binary in the background.""" - log_file = os.path.join(self.mass.storage_path, "airplay_bridge.log") self.logger.debug( "Starting Airplay bridge using config file %s", self._config_file, @@ -280,12 +280,10 @@ class AirplayProvider(PlayerProvider): "localhost", "-x", self._config_file, - "-f", - log_file, "-I", "-Z", "-d", - "all=info", + "all=warn", ] start_success = False while True: @@ -384,3 +382,17 @@ class AirplayProvider(PlayerProvider): # save config file async with aiofiles.open(self._config_file, "w") as _file: await _file.write(ET.tostring(xml_root).decode()) + + def restart_bridge(self) -> None: + """Schedule restart of bridge process.""" + if self._timer_handle is not None: + self._timer_handle.cancel() + self._timer_handle = None + + async def restart_bridge(): + self.logger.info("Restarting Airplay bridge (due to config changes)") + await self._stop_bridge() + await self._check_config_xml() + + # schedule the action for later + self._timer_handle = self.mass.loop.call_later(10, self.mass.create_task, restart_bridge) diff --git a/music_assistant/server/providers/chromecast/__init__.py b/music_assistant/server/providers/chromecast/__init__.py index c12450b4..d9523937 100644 --- a/music_assistant/server/providers/chromecast/__init__.py +++ b/music_assistant/server/providers/chromecast/__init__.py @@ -473,13 +473,15 @@ class ChromecastProvider(PlayerProvider): def _create_queue_item(queue_item: QueueItem, stream_url: str): """Create CC queue item from MA QueueItem.""" duration = int(queue_item.duration) if queue_item.duration else None - if queue_item.media_type == MediaType.TRACK: + if queue_item.media_type == MediaType.TRACK and queue_item.media_item: stream_type = STREAM_TYPE_BUFFERED metadata = { "metadataType": 3, - "albumName": queue_item.media_item.album.name, + "albumName": queue_item.media_item.album.name + if queue_item.media_item.album + else "", "songName": queue_item.media_item.name, - "artist": queue_item.media_item.artist.name, + "artist": queue_item.media_item.artist.name if queue_item.media_item.artist else "", "title": queue_item.name, "images": [{"url": queue_item.image.url}] if queue_item.image else None, } -- 2.34.1