Skip to content

Commit

Permalink
Fix Airplay playback on docker/haos installs (#1086)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelveldt authored Feb 18, 2024
1 parent 66e9e0e commit c33dde7
Show file tree
Hide file tree
Showing 6 changed files with 35 additions and 12 deletions.
7 changes: 4 additions & 3 deletions music_assistant/common/models/provider.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"""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

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
Expand Down Expand Up @@ -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)

Expand Down
26 changes: 21 additions & 5 deletions music_assistant/server/providers/airplay/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
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,
Expand Down Expand Up @@ -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, ...]:
Expand All @@ -484,13 +486,15 @@ async def handle_setup(self) -> None:
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)],
Expand All @@ -503,7 +507,19 @@ async def handle_setup(self) -> None:
},
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
Expand Down
Binary file modified music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64
100755 → 100644
Binary file not shown.
Binary file modified music_assistant/server/providers/airplay/bin/cliraop-linux-x86_64
100755 → 100644
Binary file not shown.
Binary file modified music_assistant/server/providers/airplay/bin/cliraop-macos-arm64
Binary file not shown.
14 changes: 10 additions & 4 deletions music_assistant/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ async def load_provider_manifest(provider_domain: str, provider_path: str) -> No
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")
Expand All @@ -538,9 +538,15 @@ async def load_provider_manifest(provider_domain: str, provider_path: str) -> No
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
Expand Down

0 comments on commit c33dde7

Please sign in to comment.