-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit d0d26b0
Showing
3 changed files
with
138 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
venv/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import asyncio | ||
import logging | ||
|
||
import argh | ||
import snapcast.control | ||
from mpd.asyncio import MPDClient | ||
|
||
from bidict import bidict | ||
|
||
|
||
class MPDOutput: | ||
def __init__(self, data): | ||
self.id = data['outputid'] | ||
self.name = data['outputname'] | ||
self.enabled = data['outputenabled'] == '1' | ||
|
||
|
||
class MpdSnapcastSyncer: | ||
def __init__(self, loop): | ||
self._logger = logging.getLogger(self.__class__.__name__) | ||
self._loop = loop | ||
self.mpd_outputs = {} | ||
|
||
async def setup(self, snapcast_server: str, mpd_server: str) -> None: | ||
snapcast_task = asyncio.create_task(self.setup_snapcast(snapcast_server)) | ||
mpd_task = asyncio.create_task(self.setup_mpd(mpd_server)) | ||
await snapcast_task | ||
await mpd_task | ||
|
||
def snapcast_client_changed(self, client: snapcast.control.client.Snapclient) -> None: | ||
self._loop.create_task(self.async_snapcast_client_changed(client)) | ||
|
||
async def async_snapcast_client_changed(self, client: snapcast.control.client.Snapclient) -> None: | ||
name = client.friendly_name | ||
try: | ||
output = self.mpd_outputs[name] | ||
except KeyError: | ||
# there is no output named like this Snapcast client, ignore event | ||
self._logger.debug('Ignoring change of snapcast client %s: No matching MPD output' % name) | ||
return | ||
|
||
self._logger.info('Turning %s MPD output %s' % ( | ||
'off' if self.mpd_outputs[name].enabled else 'on', | ||
name | ||
)) | ||
|
||
# determine which method to call | ||
actor = self.mpd.disableoutput if output.enabled else self.mpd.enableoutput | ||
|
||
# invert stored state of the output to avoid calling | ||
# mpd_output_changed() from mpd_outputs_changed() when MPD notifies us | ||
# about our own change | ||
self.mpd_outputs[name].enabled = not self.mpd_outputs[name].enabled | ||
|
||
# call actual actor method | ||
await actor(output.id) | ||
|
||
async def mpd_outputs_changed(self) -> None: | ||
async for output in self.mpd.outputs(): | ||
output = MPDOutput(output) | ||
try: | ||
# find stored data about this output | ||
old_output = self.mpd_outputs[output.name] | ||
except KeyError: | ||
# the output didn't exist before, don't trigger any action | ||
pass | ||
else: | ||
if output.enabled != old_output.enabled: | ||
# the output's enabled state changed | ||
await self.mpd_output_changed(output) | ||
|
||
# update our stored copy of output data | ||
self.mpd_outputs[output.name] = output | ||
|
||
async def mpd_output_changed(self, output: dict) -> None: | ||
for client in self.snapcast.clients: | ||
if client.friendly_name != output.name: | ||
continue | ||
|
||
self._logger.info('%s snapcast client %s (%s)' % ( | ||
'Unmuting' if output.enabled else 'Muting', | ||
output.name, | ||
client.identifier | ||
)) | ||
await client.set_muted(not output.enabled) | ||
return | ||
else: | ||
self._logger.debug('Ignoring change of MPD output %s: No matching snapcast client' % output.name) | ||
|
||
async def setup_snapcast(self, snapcast_server: str) -> None: | ||
self.snapcast = await snapcast.control.create_server( | ||
self._loop, | ||
snapcast_server, | ||
) | ||
|
||
for client in self.snapcast.clients: | ||
client.set_callback(self.snapcast_client_changed) | ||
self._logger.debug('Set callback for snapcast client %s' % client.friendly_name) | ||
|
||
async def setup_mpd(self, mpd_server: str) -> None: | ||
self.mpd = MPDClient() | ||
await self.mpd.connect(mpd_server) | ||
|
||
# get initial state of outputs | ||
async for output in self.mpd.outputs(): | ||
output = MPDOutput(output) | ||
self.mpd_outputs[output.name] = output | ||
|
||
# add idle command to to event loop | ||
self._loop.create_task(self.listen_mpd()) | ||
|
||
async def listen_mpd(self) -> None: | ||
async for event in self.mpd.idle(['output']): | ||
await self.mpd_outputs_changed() | ||
|
||
|
||
def main( | ||
snapcast_server: str = 'localhost', | ||
mpd_server: str = 'localhost', | ||
loglevel: bool = 'INFO'): | ||
|
||
logging.basicConfig(level=loglevel) | ||
loop = asyncio.get_event_loop() | ||
syncer = MpdSnapcastSyncer(loop) | ||
loop.run_until_complete(syncer.setup(snapcast_server, mpd_server)) | ||
loop.run_forever() | ||
|
||
|
||
if __name__ == '__main__': | ||
argh.dispatch_command(main) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
argh | ||
bidict | ||
snapcast | ||
python-mpd2 | ||
python-musicpd |