import pytest import os from .mpris import setup_mpris from .playerctl import PlayerctlCli from dbus_next.aio import MessageBus from dbus_next import Message, MessageType import asyncio from asyncio import Queue from subprocess import run as run_process async def start_playerctld(bus_address, debug=False): pkill = await asyncio.create_subprocess_shell('pkill playerctld') await pkill.wait() env = os.environ.copy() env['DBUS_SESSION_BUS_ADDRESS'] = bus_address env['G_MESSAGES_DEBUG'] = 'playerctl' proc = await asyncio.create_subprocess_shell( 'playerctld', env=env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) async def printer(stream): while True: line = await stream.readline() print(line) if not line: break if debug: asyncio.get_event_loop().create_task(printer(proc.stdout)) return proc async def get_playerctld(bus): path = '/com/github/altdesktop/playerctld' interface = 'com.github.altdesktop.playerctld' introspection = await bus.introspect('org.mpris.MediaPlayer2.playerctld', path) obj = bus.get_proxy_object(interface, path, introspection) return obj.get_interface('org.freedesktop.DBus.Properties') @pytest.mark.asyncio async def test_daemon_commands(bus_address): playerctl = PlayerctlCli(bus_address) async def run(cmd): return await playerctl.run('-p playerctld ' + cmd) # with no other players running, these should error because there's no # active player (not no players found). This tests activation and property # errors as well. results = await asyncio.gather(*(run(cmd) for cmd in ('play', 'pause', 'play-pause', 'stop', 'next', 'previous', 'position', 'volume', 'status', 'metadata', 'loop', 'shuffle'))) for result in results: assert result.returncode == 1 assert 'No player could handle this command' in result.stderr.splitlines( ) # restart playerctld so we can manage the process and see debug info playerctld_proc = await start_playerctld(bus_address) [mpris1, mpris2, mpris3] = await setup_mpris('daemon1', 'daemon2', 'daemon3', bus_address=bus_address) await mpris2.set_artist_title('artist', 'title') cmd = await run('play') assert cmd.returncode == 0, cmd.stdout assert mpris2.play_called, cmd.stdout mpris2.reset() await mpris1.set_artist_title('artist', 'title') cmd = await run('play') assert cmd.returncode == 0, cmd.stderr assert mpris1.play_called mpris1.reset() await mpris3.set_artist_title('artist', 'title') cmd = await run('play') assert cmd.returncode == 0, cmd.stderr assert mpris3.play_called mpris3.reset() await mpris3.disconnect() cmd = await run('play') assert cmd.returncode == 0, cmd.stderr assert mpris1.play_called mpris1.reset() await asyncio.gather(mpris1.disconnect(), mpris2.disconnect()) playerctld_proc.terminate() await playerctld_proc.wait() @pytest.mark.asyncio async def test_daemon_follow(bus_address): playerctld_proc = await start_playerctld(bus_address) [mpris1, mpris2] = await setup_mpris('player1', 'player2', bus_address=bus_address) playerctl = PlayerctlCli(bus_address) pctl_cmd = '--player playerctld metadata --format "{{playerInstance}}: {{artist}} - {{title}}" --follow' proc = await playerctl.start(pctl_cmd) await mpris1.set_artist_title('artist1', 'title1') line = await proc.queue.get() assert line == 'playerctld: artist1 - title1', proc.queue await mpris2.set_artist_title('artist2', 'title2') line = await proc.queue.get() assert line == 'playerctld: artist2 - title2', proc.queue [mpris3] = await setup_mpris('player3', bus_address=bus_address) await mpris3.set_artist_title('artist3', 'title3') line = await proc.queue.get() if line == '': # the line might be blank here because of the test setup line = await proc.queue.get() assert line == 'playerctld: artist3 - title3', proc.queue await mpris1.set_artist_title('artist4', 'title4') line = await proc.queue.get() assert line == 'playerctld: artist4 - title4', proc.queue await mpris1.set_artist_title('artist5', 'title5') line = await proc.queue.get() assert line == 'playerctld: artist5 - title5', proc.queue await mpris1.disconnect() line = await proc.queue.get() assert line == 'playerctld: artist3 - title3', proc.queue await asyncio.gather(mpris2.disconnect(), mpris3.disconnect()) playerctld_proc.terminate() proc.proc.terminate() await proc.proc.wait() await playerctld_proc.wait() async def playerctld_shift(bus_address, reverse=False): env = os.environ.copy() env['DBUS_SESSION_BUS_ADDRESS'] = bus_address env['G_MESSAGES_DEBUG'] = 'playerctl' cmd = 'playerctld unshift' if reverse else 'playerctld shift' shift = await asyncio.create_subprocess_shell( cmd, env=env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) return await shift.wait() @pytest.mark.asyncio async def test_daemon_shift_simple(bus_address): playerctld_proc = await start_playerctld(bus_address) mprises = await setup_mpris('player1', 'player2', 'player3', bus_address=bus_address) [mpris1, mpris2, mpris3] = mprises playerctl = PlayerctlCli(bus_address) pctl_cmd = '--player playerctld metadata --format "{{playerInstance}}: {{artist}} - {{title}}" --follow' proc = await playerctl.start(pctl_cmd) await mpris1.set_artist_title('artist1', 'title1') line = await proc.queue.get() assert line == 'playerctld: artist1 - title1', proc.queue await mpris2.set_artist_title('artist2', 'title2') line = await proc.queue.get() assert line == 'playerctld: artist2 - title2', proc.queue await mpris3.set_artist_title('artist3', 'title3') line = await proc.queue.get() assert line == 'playerctld: artist3 - title3', proc.queue code = await playerctld_shift(bus_address) assert code == 0 line = await proc.queue.get() assert line == 'playerctld: artist2 - title2', proc.queue code = await playerctld_shift(bus_address) assert code == 0 line = await proc.queue.get() assert line == 'playerctld: artist1 - title1', proc.queue code = await playerctld_shift(bus_address, reverse=True) assert code == 0 line = await proc.queue.get() assert line == 'playerctld: artist2 - title2', proc.queue code = await playerctld_shift(bus_address, reverse=True) assert code == 0 line = await proc.queue.get() assert line == 'playerctld: artist3 - title3', proc.queue playerctld_proc.terminate() proc.proc.terminate() await asyncio.gather(mpris1.disconnect(), mpris2.disconnect(), playerctld_proc.wait(), proc.proc.wait()) @pytest.mark.asyncio async def test_daemon_shift_no_player(bus_address): playerctld_proc = await start_playerctld(bus_address) playerctl = PlayerctlCli(bus_address) pctl_cmd = '--player playerctld metadata --format "{{playerInstance}}: {{artist}} - {{title}}" --follow' proc = await playerctl.start(pctl_cmd) code = await playerctld_shift(bus_address) assert code == 1 [mpris1] = await setup_mpris('player1', bus_address=bus_address) code = await playerctld_shift(bus_address) assert code == 0 await mpris1.disconnect() code = await playerctld_shift(bus_address) assert code == 1 code = await playerctld_shift(bus_address, reverse=True) assert code == 1 [mpris1] = await setup_mpris('player1', bus_address=bus_address) code = await playerctld_shift(bus_address, reverse=True) assert code == 0 await mpris1.disconnect() code = await playerctld_shift(bus_address, reverse=True) assert code == 1 playerctld_proc.terminate() await playerctld_proc.wait() @pytest.mark.asyncio async def test_active_player_change(bus_address): queue = Queue() playerctld_proc = await start_playerctld(bus_address) bus = await MessageBus(bus_address=bus_address).connect() reply = await bus.call( Message(destination='org.freedesktop.DBus', interface='org.freedesktop.DBus', path='/org/freedesktop/DBus', member='AddMatch', signature='s', body=["sender='org.mpris.MediaPlayer2.playerctld'"])) assert reply.message_type == MessageType.METHOD_RETURN, reply.body def message_handler(message): if message.member == 'PropertiesChanged' and message.body[ 0] == 'com.github.altdesktop.playerctld' and 'PlayerNames' in message.body[ 1]: queue.put_nowait(message.body[1]['PlayerNames'].value) def player_list(*args): return [f'org.mpris.MediaPlayer2.{name}' for name in args] bus.add_message_handler(message_handler) [mpris1] = await setup_mpris('player1', bus_address=bus_address) assert player_list('player1') == await queue.get() [mpris2] = await setup_mpris('player2', bus_address=bus_address) assert player_list('player2', 'player1') == await queue.get() # changing artist/title should bump the player up await mpris1.set_artist_title('artist1', 'title1', '/1') assert player_list('player1', 'player2') == await queue.get() # if properties are not actually different, it shouldn't update await mpris2.set_artist_title('artist2', 'title2', '/2') assert player_list('player2', 'player1') == await queue.get() await mpris1.set_artist_title('artist1', 'title1', '/1') await mpris1.ping() assert queue.empty() bus.disconnect() await asyncio.gather(mpris1.disconnect(), mpris2.disconnect(), bus.wait_for_disconnect()) playerctld_proc.terminate() await playerctld_proc.wait()