Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 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
574 changes: 574 additions & 0 deletions music_assistant/providers/airplay/airplay2.py

Large diffs are not rendered by default.

Binary file modified music_assistant/providers/airplay/bin/cliraop-linux-aarch64
Binary file not shown.
7 changes: 7 additions & 0 deletions music_assistant/providers/airplay/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
CONF_READ_AHEAD_BUFFER: Final[str] = "read_ahead_buffer"
CONF_IGNORE_VOLUME: Final[str] = "ignore_volume"
CONF_CREDENTIALS: Final[str] = "credentials"
CONF_AIRPLAY_VERSION: Final[str] = "airplay_version"

AIRPLAY2_DISCOVERY_TYPE: Final[str] = "_airplay._tcp.local."
RAOP_DISCOVERY_TYPE: Final[str] = "_raop._tcp.local."

AIRPLAY2_MIN_LOG_LEVEL: Final[int] = 3 # Min loglevel to ensure stderr output contains what we need

BACKOFF_TIME_LOWER_LIMIT: Final[int] = 15 # seconds
BACKOFF_TIME_UPPER_LIMIT: Final[int] = 300 # Five minutes
Expand Down Expand Up @@ -47,4 +53,5 @@
("Sonos", "Arc Ultra"),
# Samsung has been repeatedly being reported as having issues with AirPlay 1/raop
("Samsung", "*"),
("Ubiquiti Inc.", "UPL-AMP"),
)
42 changes: 32 additions & 10 deletions music_assistant/providers/airplay/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ def get_model_info(info: AsyncServiceInfo) -> tuple[str, str]:
return ("Apple", "Apple TV 4K Gen2")
if model == "AppleTV14,1":
return ("Apple", "Apple TV 4K Gen3")
if model == "UPL-AMP":
return ("Ubiquiti Inc.", "UPL-AMP")
if "AirPort" in model:
return ("Apple", "AirPort Express")
if "AudioAccessory" in model:
Expand Down Expand Up @@ -89,29 +91,49 @@ def is_broken_raop_model(manufacturer: str, model: str) -> bool:
return False


async def get_cliraop_binary() -> str:
async def get_cli_binary(version: int) -> str:
"""Find the correct raop/airplay binary belonging to the platform."""

async def check_binary(cliraop_path: str) -> str | None:
async def check_binary(cli_path: str) -> str | None:
try:
returncode, output = await check_output(
cliraop_path,
"-check",
)
if returncode == 0 and output.strip().decode() == "cliraop check":
return cliraop_path
if version == 1:
args = [
cli_path,
"-check",
]
passing_output = "cliraop check"
else:
config_file = os.path.join(os.path.dirname(__file__), "bin", "cliap2.conf")
args = [
cli_path,
"--testrun",
"--config",
config_file,
]

returncode, output = await check_output(*args)
if (version == 1 and returncode == 0 and output.strip().decode() == passing_output) or (
version == 2 and returncode == 0
):
return cli_path
except OSError:
pass
return None

base_path = os.path.join(os.path.dirname(__file__), "bin")
system = platform.system().lower().replace("darwin", "macos")
architecture = platform.machine().lower()
if version == 1:
package = "cliraop"
elif version == 2:
package = "cliap2"
else:
raise RuntimeError(f"Unsupported CLI binary version requested: {version}")

if bridge_binary := await check_binary(
os.path.join(base_path, f"cliraop-{system}-{architecture}")
os.path.join(base_path, f"{package}-{system}-{architecture}")
):
return bridge_binary

msg = f"Unable to locate RAOP Play binary for {system}/{architecture}"
msg = f"Unable to locate RAOP/AirPlay2 Play binary {package} for {system}/{architecture}"
raise RuntimeError(msg)
2 changes: 1 addition & 1 deletion music_assistant/providers/airplay/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
"multi_instance": false,
"builtin": false,
"icon": "cast-variant",
"mdns_discovery": ["_raop._tcp.local."]
"mdns_discovery": ["_raop._tcp.local.", "_airplay._tcp.local."]
}
Loading