From: Marcel van der Veldt Date: Sun, 18 Feb 2024 14:20:25 +0000 (+0100) Subject: Fix Airplay playback on docker/haos installs (#1086) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=c33dde70bf3cd35e35773da58c7a0a0710a0ea6e;p=music-assistant-server.git Fix Airplay playback on docker/haos installs (#1086) --- diff --git a/music_assistant/common/models/provider.py b/music_assistant/common/models/provider.py index 6370e502..73279443 100644 --- a/music_assistant/common/models/provider.py +++ b/music_assistant/common/models/provider.py @@ -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) diff --git a/music_assistant/server/providers/airplay/__init__.py b/music_assistant/server/providers/airplay/__init__.py index a2738706..98042efd 100644 --- a/music_assistant/server/providers/airplay/__init__.py +++ b/music_assistant/server/providers/airplay/__init__.py @@ -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 diff --git a/music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64 b/music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64 old mode 100755 new mode 100644 index ef4c4eb4..0bd6d271 Binary files a/music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64 and b/music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64 differ diff --git a/music_assistant/server/providers/airplay/bin/cliraop-linux-x86_64 b/music_assistant/server/providers/airplay/bin/cliraop-linux-x86_64 old mode 100755 new mode 100644 index ba25f08a..eefe426a 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 diff --git a/music_assistant/server/providers/airplay/bin/cliraop-macos-arm64 b/music_assistant/server/providers/airplay/bin/cliraop-macos-arm64 index 4a91d903..e04e68da 100755 Binary files a/music_assistant/server/providers/airplay/bin/cliraop-macos-arm64 and b/music_assistant/server/providers/airplay/bin/cliraop-macos-arm64 differ diff --git a/music_assistant/server/server.py b/music_assistant/server/server.py index 0c808048..43309e61 100644 --- a/music_assistant/server/server.py +++ b/music_assistant/server/server.py @@ -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