Skip to content

Commit

Permalink
Replace Airplay provider (#1084)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelveldt authored Feb 18, 2024
1 parent adfc661 commit 921650b
Show file tree
Hide file tree
Showing 15 changed files with 997 additions and 444 deletions.
11 changes: 6 additions & 5 deletions music_assistant/common/models/media_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@
from mashumaro import DataClassDictMixin

from music_assistant.common.helpers.uri import create_uri
from music_assistant.common.helpers.util import (
create_sort_name,
is_valid_uuid,
merge_lists,
)
from music_assistant.common.helpers.util import create_sort_name, is_valid_uuid, merge_lists
from music_assistant.common.models.enums import (
AlbumType,
ContentType,
Expand Down Expand Up @@ -355,6 +351,11 @@ def image(self) -> MediaItemImage | None:
return self.album.image
return super().image

@property
def artist_str(self) -> str:
"""Return (combined) artist string for track."""
return "/".join(x.name for x in self.artists)


@dataclass(kw_only=True)
class AlbumTrack(Track):
Expand Down
29 changes: 19 additions & 10 deletions music_assistant/server/controllers/players.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,15 +401,6 @@ async def cmd_power(self, player_id: str, powered: bool) -> None:
if player.powered == powered:
return # nothing to do

# inform (active) group player if needed
# NOTE: this must be on the top to prevent race conditions
if active_group_player := self._get_active_player_group(player):
if active_group_player.player_id.startswith(SYNCGROUP_PREFIX):
self._on_syncgroup_child_power(
active_group_player.player_id, player.player_id, powered
)
elif player_prov := self.get_player_provider(active_group_player.player_id):
player_prov.on_child_power(active_group_player.player_id, player.player_id, powered)
# stop player at power off
if (
not powered
Expand Down Expand Up @@ -437,8 +428,16 @@ async def cmd_power(self, player_id: str, powered: bool) -> None:
# as fast as possible and prevent race conditions
player.powered = powered
self.update(player_id)
# handle actions when a syncgroup child turns on
if active_group_player := self._get_active_player_group(player):
if active_group_player.player_id.startswith(SYNCGROUP_PREFIX):
self._on_syncgroup_child_power(
active_group_player.player_id, player.player_id, powered
)
elif player_prov := self.get_player_provider(active_group_player.player_id):
player_prov.on_child_power(active_group_player.player_id, player.player_id, powered)
# handle 'auto play on power on' feature
if (
elif (
powered
and self.mass.config.get_raw_player_config_value(player_id, CONF_AUTO_PLAY, False)
and player.active_source in (None, player_id)
Expand Down Expand Up @@ -899,6 +898,16 @@ def get_sync_leader(self, group_player: Player) -> Player | None:
continue
elif child_player.group_childs:
return child_player
# select new sync leader: return the first playing player
for child_player in self.iter_group_members(
group_player, only_powered=True, only_playing=True
):
return child_player
# fallback select new sync leader: return the first powered player
for child_player in self.iter_group_members(
group_player, only_powered=True, only_playing=False
):
return child_player
return None

async def _sync_syncgroup(self, player_id: str) -> None:
Expand Down
54 changes: 35 additions & 19 deletions music_assistant/server/helpers/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,32 +42,22 @@ def __init__(

async def __aenter__(self) -> AsyncProcess:
"""Enter context manager."""
await self.start()
return self

async def __aexit__(self, exc_type, exc_value, traceback) -> bool:
"""Exit context manager."""
await self.close()

async def start(self) -> None:
"""Perform Async init of process."""
self._proc = await asyncio.create_subprocess_exec(
*self._args,
stdin=asyncio.subprocess.PIPE if self._enable_stdin else None,
stdout=asyncio.subprocess.PIPE if self._enable_stdout else None,
stderr=asyncio.subprocess.PIPE if self._enable_stderr else None,
close_fds=True,
)
return self

async def __aexit__(self, exc_type, exc_value, traceback) -> bool:
"""Exit context manager."""
self.closed = True
# make sure the process is cleaned up
self.write_eof()
if self._proc.returncode is None:
try:
async with asyncio.timeout(10):
await self._proc.communicate()
except TimeoutError:
self._proc.kill()
await self._proc.communicate()
if self._proc.returncode is None:
self._proc.kill()
if self._attached_task and not self._attached_task.done():
with suppress(asyncio.CancelledError):
self._attached_task.cancel()

async def iter_chunked(self, n: int = DEFAULT_CHUNKSIZE) -> AsyncGenerator[bytes, None]:
"""Yield chunks of n size from the process stdout."""
Expand Down Expand Up @@ -113,6 +103,8 @@ async def write(self, data: bytes) -> None:

def write_eof(self) -> None:
"""Write end of file to to process stdin."""
if not self._enable_stdin:
return
if self.closed or self._proc.stdin.is_closing():
return
try:
Expand All @@ -128,6 +120,30 @@ def write_eof(self) -> None:
# already exited, race condition
pass

async def close(self) -> None:
"""Close/terminate the process."""
self.closed = True
if self._attached_task and not self._attached_task.done():
with suppress(asyncio.CancelledError):
self._attached_task.cancel()
# make sure the process is cleaned up
self.write_eof()
if self._proc.returncode is None:
try:
async with asyncio.timeout(10):
await self._proc.communicate()
except TimeoutError:
self._proc.kill()
await self.wait()

async def wait(self) -> int:
"""Wait for the process and return the returncode."""
if self._proc.returncode is not None:
return self._proc.returncode
exitcode = await self._proc.wait()
self.closed = True
return exitcode

async def communicate(self, input_data: bytes | None = None) -> tuple[bytes, bytes]:
"""Write bytes to process and read back results."""
return await self._proc.communicate(input_data)
Expand Down
Loading

0 comments on commit 921650b

Please sign in to comment.