From: Marcel van der Veldt Date: Fri, 7 Jul 2023 22:59:56 +0000 (+0200) Subject: Fix playback issues on Sonos and DLNA (#747) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=e4ac159cd6f9872eda9f0c3dbf2e5327d790fd1a;p=music-assistant-server.git Fix playback issues on Sonos and DLNA (#747) * Fix enqueue of next track on sonos * fix hiding of sonos players by default in dlna * improve player names on airplay * fix playback flow mode on dlna players * disable TV's by default * fix error in logs from dlna --- diff --git a/music_assistant/server/controllers/config.py b/music_assistant/server/controllers/config.py index 5c10de81..af7718bc 100644 --- a/music_assistant/server/controllers/config.py +++ b/music_assistant/server/controllers/config.py @@ -412,7 +412,9 @@ class ConfigController: assert isinstance(provider, PlayerProvider) provider.on_player_config_removed(player_id) - def create_default_player_config(self, player_id: str, provider: str, name: str) -> None: + def create_default_player_config( + self, player_id: str, provider: str, name: str, enabled: bool + ) -> None: """ Create default/empty PlayerConfig. @@ -427,7 +429,7 @@ class ConfigController: # config does not yet exist, create a default one conf_key = f"{CONF_PLAYERS}/{player_id}" default_conf = PlayerConfig( - values={}, provider=provider, player_id=player_id, default_name=name + values={}, provider=provider, player_id=player_id, enabled=enabled, default_name=name ) self.set( conf_key, diff --git a/music_assistant/server/controllers/players.py b/music_assistant/server/controllers/players.py index 428105df..605e6d8e 100755 --- a/music_assistant/server/controllers/players.py +++ b/music_assistant/server/controllers/players.py @@ -124,7 +124,9 @@ class PlayerController(CoreController): raise AlreadyRegisteredError(f"Player {player_id} is already registered") # make sure a default config exists - self.mass.config.create_default_player_config(player_id, player.provider, player.name) + self.mass.config.create_default_player_config( + player_id, player.provider, player.name, player.enabled_by_default + ) player.enabled = self.mass.config.get(f"{CONF_PLAYERS}/{player_id}/enabled", True) diff --git a/music_assistant/server/helpers/didl_lite.py b/music_assistant/server/helpers/didl_lite.py index b537d4a4..7a8f0edd 100644 --- a/music_assistant/server/helpers/didl_lite.py +++ b/music_assistant/server/helpers/didl_lite.py @@ -22,6 +22,7 @@ def create_didl_metadata( if queue_item is None: return ( '' + f'' "Music Assistant" f"{escape_string(MASS_LOGO_ONLINE)}" "object.item.audioItem.audioBroadcast" diff --git a/music_assistant/server/providers/airplay/__init__.py b/music_assistant/server/providers/airplay/__init__.py index 6e92d7ac..7254e5fa 100644 --- a/music_assistant/server/providers/airplay/__init__.py +++ b/music_assistant/server/providers/airplay/__init__.py @@ -10,6 +10,7 @@ import asyncio import os import platform import xml.etree.ElementTree as ET # noqa: N817 +from contextlib import suppress from typing import TYPE_CHECKING import aiofiles @@ -238,7 +239,7 @@ class AirplayProvider(PlayerProvider): slimproto_prov = self.mass.get_provider("slimproto") await slimproto_prov.cmd_unsync(player_id) - def _handle_player_register_callback(self, player: Player) -> None: + async def _handle_player_register_callback(self, player: Player) -> None: """Handle player register callback from slimproto source player.""" # TODO: Can we get better device info from mDNS ? player.provider = self.domain @@ -249,6 +250,30 @@ class AirplayProvider(PlayerProvider): ) player.supports_24bit = False + # extend info from the discovery xml + async with aiofiles.open(self._config_file, "r") as _file: + xml_data = await _file.read() + with suppress(ET.ParseError): + xml_root = ET.XML(xml_data) + for device_elem in xml_root.findall("device"): + player_id = device_elem.find("mac").text + if player_id != player.player_id: + continue + # prefer name from UDN because default name is often wrong + udn = device_elem.find("udn").text + udn_name = udn.split("@")[1].split("._")[0] + player.name = udn_name + # disable sonos by default + if "sonos" in (device_elem.find("friendly_name").text or "").lower(): + player.enabled_by_default = False + # TODO: query more info directly from the device + player.device_info = DeviceInfo( + model="Airplay device", + address=player.device_info.address, + manufacturer="SONOS", + ) + break + def _handle_player_update_callback(self, player: Player) -> None: """Handle player update callback from slimproto source player.""" # we could override anything on the player object here diff --git a/music_assistant/server/providers/chromecast/__init__.py b/music_assistant/server/providers/chromecast/__init__.py index 5461d29d..97cf07f5 100644 --- a/music_assistant/server/providers/chromecast/__init__.py +++ b/music_assistant/server/providers/chromecast/__init__.py @@ -329,6 +329,13 @@ class ChromecastProvider(PlayerProvider): ) return + # Disable TV's by default + # (can be enabled manually by the user) + enabled_by_default = True + for exclude in ("tv", "/12", "PUS", "OLED"): + if exclude.lower() in cast_info.friendly_name.lower(): + enabled_by_default = False + # Instantiate chromecast object castplayer = CastPlayer( player_id, @@ -355,6 +362,7 @@ class ChromecastProvider(PlayerProvider): PlayerFeature.VOLUME_SET, ), max_sample_rate=96000, + enabled_by_default=enabled_by_default, ), logger=self.logger.getChild(cast_info.friendly_name), ) diff --git a/music_assistant/server/providers/dlna/__init__.py b/music_assistant/server/providers/dlna/__init__.py index 4949e169..c7848eec 100644 --- a/music_assistant/server/providers/dlna/__init__.py +++ b/music_assistant/server/providers/dlna/__init__.py @@ -276,7 +276,8 @@ class DLNAPlayerProvider(PlayerProvider): await self.cmd_stop(player_id) didl_metadata = create_didl_metadata(self.mass, url, queue_item) - await dlna_player.device.async_set_transport_uri(url, queue_item.name, didl_metadata) + title = queue_item.name if queue_item else "Music Assistant" + await dlna_player.device.async_set_transport_uri(url, title, didl_metadata) # Play it await dlna_player.device.async_wait_for_can_play(10) await dlna_player.device.async_play() diff --git a/music_assistant/server/providers/slimproto/__init__.py b/music_assistant/server/providers/slimproto/__init__.py index 7e841f16..9a6c704c 100644 --- a/music_assistant/server/providers/slimproto/__init__.py +++ b/music_assistant/server/providers/slimproto/__init__.py @@ -5,7 +5,7 @@ import asyncio import statistics import time from collections import deque -from collections.abc import Callable, Generator +from collections.abc import Callable, Coroutine, Generator from contextlib import suppress from dataclasses import dataclass from typing import TYPE_CHECKING, Any @@ -170,7 +170,7 @@ class SlimprotoProvider(PlayerProvider): _socket_servers: list[asyncio.Server | asyncio.BaseTransport] _socket_clients: dict[str, SlimClient] _sync_playpoints: dict[str, deque[SyncPlayPoint]] - _virtual_providers: dict[str, tuple[Callable, Callable]] + _virtual_providers: dict[str, tuple[Coroutine, Callable]] _do_not_resync_before: dict[str, float] _cli: LmsCli port: int = DEFAULT_SLIMPROTO_PORT @@ -271,7 +271,7 @@ class SlimprotoProvider(PlayerProvider): return # forward player update to MA player controller - self._handle_player_update(client) + self.mass.create_task(self._handle_player_update(client)) # construct SlimClient from socket client SlimClient(reader, writer, client_callback) @@ -535,7 +535,7 @@ class SlimprotoProvider(PlayerProvider): def register_virtual_provider( self, player_model: str, - register_callback: Callable, + register_callback: Coroutine, update_callback: Callable, ) -> None: """Register a virtual provider based on slimproto, such as the airplay bridge.""" @@ -551,7 +551,7 @@ class SlimprotoProvider(PlayerProvider): """Unregister a virtual provider.""" self._virtual_providers.pop(player_model, None) - def _handle_player_update(self, client: SlimClient) -> None: + async def _handle_player_update(self, client: SlimClient) -> None: """Process SlimClient update/add to Player controller.""" player_id = client.player_id virtual_provider_info = self._virtual_providers.get(client.device_model) @@ -580,7 +580,7 @@ class SlimprotoProvider(PlayerProvider): ) if virtual_provider_info: # if this player is part of a virtual provider run the callback - virtual_provider_info[0](player) + await virtual_provider_info[0](player) self.mass.players.register_or_update(player) # update player state on player events @@ -767,12 +767,12 @@ class SlimprotoProvider(PlayerProvider): self._socket_clients[player_id] = client # update all attributes - self._handle_player_update(client) + await self._handle_player_update(client) # update existing players so they can update their `can_sync_with` field for item in self._socket_clients.values(): if item.player_id == player_id: continue - self._handle_player_update(item) + await self._handle_player_update(item) # restore volume and power state if last_state := await self.mass.cache.get(f"{CACHE_KEY_PREV_STATE}.{player_id}"): init_power = last_state[0] diff --git a/music_assistant/server/providers/sonos/__init__.py b/music_assistant/server/providers/sonos/__init__.py index 2386a465..fb0ef2db 100644 --- a/music_assistant/server/providers/sonos/__init__.py +++ b/music_assistant/server/providers/sonos/__init__.py @@ -582,7 +582,8 @@ class SonosPlayerProvider(PlayerProvider): # enqueue next item if needed if sonos_player.player.state == PlayerState.PLAYING and ( - sonos_player.next_url or sonos_player.next_url == sonos_player.player.current_url + sonos_player.next_url is None + or sonos_player.next_url == sonos_player.player.current_url ): self.mass.create_task(self._enqueue_next_track(sonos_player))