Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jplitza committed Oct 9, 2020
0 parents commit d0d26b0
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
venv/
132 changes: 132 additions & 0 deletions main.py
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)
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
argh
bidict
snapcast
python-mpd2
python-musicpd

0 comments on commit d0d26b0

Please sign in to comment.