Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Airplay playback on docker/haos installs #1086

Merged
merged 3 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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