Some small bugfixes and improvements (#532)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 14 Mar 2023 22:59:31 +0000 (23:59 +0100)
committerGitHub <noreply@github.com>
Tue, 14 Mar 2023 22:59:31 +0000 (23:59 +0100)
- [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)

music_assistant/common/models/config_entries.py
music_assistant/server/controllers/media/albums.py
music_assistant/server/controllers/media/artists.py
music_assistant/server/controllers/media/base.py
music_assistant/server/controllers/media/tracks.py
music_assistant/server/controllers/music.py
music_assistant/server/providers/airplay/__init__.py
music_assistant/server/providers/chromecast/__init__.py

index 694de33c7dfba1705363fae5825885a1da2236d2..b5e9f54344686bf77cc4f09f2da1bdff0f0b85cf 100644 (file)
@@ -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
 
index 35f4520a762fd1c749df349a47db889ee6c19aad..22fc4ee6545c3ee9150093d9dd73d59eb14400f4 100644 (file)
@@ -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,
index 000f857bdb756faa62771f4efb84345f5a5351b7..a81acd8a3e38c503686880c22bc573178f87803e 100644 (file)
@@ -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,
index 3dd67d16b96a2e49ada6d94bbe4dfede1b6e1cc6..6464fd2773a2f0eafc8dd897f6e0a1222e8233c1 100644 (file)
@@ -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:
index ad0c3937a9f9f5dbe6cd8e5822fa5173bfce9437..46c867d50f13c9956180857185858ace7280edaa 100644 (file)
@@ -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)
index d0321e74c440123e155744caf4f239910f3c04d4..ca112e4243914d3c53bb178f7fc71cc5ca42eb4b 100755 (executable)
@@ -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 []
 
index da4f76e6d9728df4313aa7b5e2693c1c9ecb4e3f..3d43440358d124391eac990ad4976c5ff4e5d6d4 100644 (file)
@@ -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)
index c12450b4e1ac8881e093d43a195ca05df1c84b38..d9523937107ac166b87ccaf1fe4d4d7cd2c360ed 100644 (file)
@@ -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,
             }