Fix Airplay playback on docker/haos installs (#1086)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 18 Feb 2024 14:20:25 +0000 (15:20 +0100)
committerGitHub <noreply@github.com>
Sun, 18 Feb 2024 14:20:25 +0000 (15:20 +0100)
music_assistant/common/models/provider.py
music_assistant/server/providers/airplay/__init__.py
music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64 [changed mode: 0755->0644]
music_assistant/server/providers/airplay/bin/cliraop-linux-x86_64 [changed mode: 0755->0644]
music_assistant/server/providers/airplay/bin/cliraop-macos-arm64
music_assistant/server/server.py

index 6370e502a88338fa5b2925b4cbfef98b5b50ab1b..7327944348234d492305352b81b71deb6804524e 100644 (file)
@@ -1,6 +1,7 @@
 """Models for providers and plugins in the MA ecosystem."""
+from __future__ import annotations
 
-import asyncio
+import asyncio  # noqa: TCH003
 from dataclasses import dataclass, field
 from typing import Any, TypedDict
 
@@ -8,7 +9,7 @@ from mashumaro.mixins.orjson import DataClassORJSONMixin
 
 from music_assistant.common.helpers.json import load_json_file
 
-from .enums import MediaType, ProviderFeature, ProviderType
+from .enums import MediaType, ProviderFeature, ProviderType  # noqa: TCH001
 
 
 @dataclass
@@ -49,7 +50,7 @@ class ProviderManifest(DataClassORJSONMixin):
     icon_svg_dark: str | None = None
 
     @classmethod
-    async def parse(cls: "ProviderManifest", manifest_file: str) -> "ProviderManifest":
+    async def parse(cls: ProviderManifest, manifest_file: str) -> ProviderManifest:
         """Parse ProviderManifest from file."""
         return await load_json_file(manifest_file, ProviderManifest)
 
index a273870651380a4ce383fb4b43dc07a69702836a..98042efd2388e9016a77b3b7e926ef8e7094008c 100644 (file)
@@ -20,7 +20,7 @@ from pyatv.protocols.raop import RaopStream
 from zeroconf import ServiceInfo
 
 from music_assistant.common.helpers.datetime import utc
-from music_assistant.common.helpers.util import get_ip_pton
+from music_assistant.common.helpers.util import get_ip_pton, select_free_port
 from music_assistant.common.models.config_entries import (
     CONF_ENTRY_CROSSFADE,
     CONF_ENTRY_CROSSFADE_DURATION,
@@ -472,6 +472,8 @@ class AirplayProvider(PlayerProvider):
     _discovery_running: bool = False
     _cliraop_bin: str | None = None
     _stream_tasks: dict[str, asyncio.Task]
+    _dacp_server: asyncio.Server = None
+    _dacp_info: ServiceInfo = None
 
     @property
     def supported_features(self) -> tuple[ProviderFeature, ...]:
@@ -484,13 +486,15 @@ class AirplayProvider(PlayerProvider):
         self._stream_tasks = {}
         self._cliraop_bin = await self.get_cliraop_binary()
         self.mass.create_task(self._run_discovery())
-        dacp_port = 49831
+        dacp_port = await select_free_port(39831, 49831)
         self.dacp_id = dacp_id = f"{randrange(2 ** 64):X}"
         self.logger.debug("Starting DACP ActiveRemote %s on port %s", dacp_id, dacp_port)
-        await asyncio.start_server(self._handle_dacp_request, "0.0.0.0", dacp_port)
+        self._dacp_server = await asyncio.start_server(
+            self._handle_dacp_request, "0.0.0.0", dacp_port
+        )
         zeroconf_type = "_dacp._tcp.local."
         server_id = f"iTunes_Ctrl_{dacp_id}.{zeroconf_type}"
-        info = ServiceInfo(
+        self._dacp_info = ServiceInfo(
             zeroconf_type,
             name=server_id,
             addresses=[await get_ip_pton(self.mass.streams.publish_ip)],
@@ -503,7 +507,19 @@ class AirplayProvider(PlayerProvider):
             },
             server=f"{socket.gethostname()}.local",
         )
-        await self.mass.zeroconf.async_register_service(info)
+        await self.mass.zeroconf.async_register_service(self._dacp_info)
+
+    async def unload(self) -> None:
+        """Handle close/cleanup of the provider."""
+        # power off all players (will disconnct and close cliraop)
+        for player_id in self._atv_players:
+            await self.cmd_power(player_id, False)
+        # shutdown DACP server
+        if self._dacp_server:
+            self._dacp_server.close()
+        # shutdown DACP zeroconf service
+        if self._dacp_info:
+            await self.mass.zeroconf.async_unregister_service(self._dacp_info)
 
     async def _handle_dacp_request(  # noqa: PLR0915
         self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
old mode 100755 (executable)
new mode 100644 (file)
index ef4c4eb..0bd6d27
Binary files a/music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64 and b/music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64 differ
old mode 100755 (executable)
new mode 100644 (file)
index ba25f08..eefe426
Binary files a/music_assistant/server/providers/airplay/bin/cliraop-linux-x86_64 and b/music_assistant/server/providers/airplay/bin/cliraop-linux-x86_64 differ
index 4a91d903f38ee2044ca1be0bcde347464279f29f..e04e68da98171d519c931d7d138298ea1e0165a1 100755 (executable)
Binary files a/music_assistant/server/providers/airplay/bin/cliraop-macos-arm64 and b/music_assistant/server/providers/airplay/bin/cliraop-macos-arm64 differ
index 0c80804843e65bff6cb48ec8869f375b4c7941eb..43309e6103581e74c3421985dbfe019a954ccdf6 100644 (file)
@@ -527,7 +527,7 @@ class MusicAssistant:
                 if file_str != "manifest.json":
                     continue
                 try:
-                    provider_manifest = await ProviderManifest.parse(file_path)
+                    provider_manifest: ProviderManifest = await ProviderManifest.parse(file_path)
                     # check for icon.svg file
                     if not provider_manifest.icon_svg:
                         icon_path = os.path.join(provider_path, "icon.svg")
@@ -538,9 +538,15 @@ class MusicAssistant:
                         icon_path = os.path.join(provider_path, "icon_dark.svg")
                         if os.path.isfile(icon_path):
                             provider_manifest.icon_svg_dark = await get_icon_string(icon_path)
-                    # install requirements
-                    for requirement in provider_manifest.requirements:
-                        await install_package(requirement)
+                    # try to load the module
+                    try:
+                        await get_provider_module(provider_manifest.domain)
+                    except ImportError:
+                        # install requirements
+                        for requirement in provider_manifest.requirements:
+                            await install_package(requirement)
+                        # try loading the provider again to be safe
+                        await get_provider_module(provider_manifest.domain)
                     self._provider_manifests[provider_manifest.domain] = provider_manifest
                     LOGGER.debug("Loaded manifest for provider %s", provider_manifest.name)
                 except Exception as exc:  # pylint: disable=broad-except