2018-11-03 23:19:03 +13:00
|
|
|
|
|
|
|
|
from threading import Thread
|
2018-11-04 14:41:08 +13:00
|
|
|
import os, logging, subprocess, socket, json, random, string, select
|
2018-11-03 23:19:03 +13:00
|
|
|
|
|
|
|
|
# import logger
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
class IrisSnapcast(object):
|
|
|
|
|
|
|
|
|
|
socket = None
|
|
|
|
|
listen = False
|
|
|
|
|
|
|
|
|
|
def __init__(self, config):
|
|
|
|
|
self.config = config
|
|
|
|
|
self.path = os.path.dirname(__file__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
|
# This thread has been started
|
|
|
|
|
##
|
|
|
|
|
def connect(self):
|
|
|
|
|
|
|
|
|
|
host = str(self.config['iris']['snapcast_host'])
|
|
|
|
|
port = int(self.config['iris']['snapcast_port'])
|
|
|
|
|
|
|
|
|
|
try:
|
2018-11-04 14:41:08 +13:00
|
|
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
|
self.sock.settimeout(10)
|
|
|
|
|
self.sock.connect((host, port))
|
2018-11-03 23:19:03 +13:00
|
|
|
logger.debug("Snapcast connection established on "+host+":"+str(port))
|
|
|
|
|
|
|
|
|
|
except socket.gaierror, e:
|
|
|
|
|
raise Exception(e);
|
|
|
|
|
|
|
|
|
|
except socket.error, e:
|
|
|
|
|
raise Exception(e);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
|
# Listen for socket activity
|
|
|
|
|
# Broadcast events to the piped broadcaster
|
|
|
|
|
#
|
|
|
|
|
# @param broadcast = Method
|
|
|
|
|
##
|
|
|
|
|
def listen(self, broadcast):
|
|
|
|
|
|
2018-11-04 14:41:08 +13:00
|
|
|
self.connect()
|
2018-11-03 23:19:03 +13:00
|
|
|
self.listen = True
|
|
|
|
|
|
2018-11-04 14:41:08 +13:00
|
|
|
logger.info("Established Snapcast listener")
|
|
|
|
|
broadcast(data={'method':'snapcast_connected'})
|
|
|
|
|
|
|
|
|
|
messages = []
|
2018-11-03 23:19:03 +13:00
|
|
|
data = ""
|
2018-11-04 14:41:08 +13:00
|
|
|
select.select([], [self.sock], [])
|
2018-11-03 23:19:03 +13:00
|
|
|
|
|
|
|
|
while self.listen:
|
|
|
|
|
|
2018-11-05 16:33:36 +13:00
|
|
|
# This loop will run constantly in the background, so we need a relatively short
|
|
|
|
|
# timeout as this would otherwise block the shutdown sequence
|
|
|
|
|
timeout = 1
|
|
|
|
|
|
|
|
|
|
readlist, writelist, exceptionlist = select.select([self.sock], [], [], timeout)
|
2018-11-03 23:19:03 +13:00
|
|
|
|
2018-11-04 14:41:08 +13:00
|
|
|
# Check if we've got any lists
|
|
|
|
|
if [readlist, writelist, exceptionlist] != [[], [], []]:
|
2018-11-03 23:19:03 +13:00
|
|
|
|
2018-11-04 14:41:08 +13:00
|
|
|
# Rread and print the available data on any of the read list
|
|
|
|
|
for socket in readlist:
|
2018-11-03 23:19:03 +13:00
|
|
|
|
2018-11-05 16:33:36 +13:00
|
|
|
# Allow a relatively large buffer size to handle large JSON payloads
|
|
|
|
|
message = socket.recv(8192)
|
2018-11-04 14:41:08 +13:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
message = json.loads(message)
|
|
|
|
|
|
|
|
|
|
# Prefix with snapcast
|
|
|
|
|
message['method'] = "snapcast_"+message['method']
|
|
|
|
|
|
|
|
|
|
# Broadcast to all clients
|
|
|
|
|
broadcast(data=message)
|
|
|
|
|
except:
|
|
|
|
|
logger.error("Malformed Snapcast message: "+message)
|
2018-11-03 23:19:03 +13:00
|
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
|
# Stop our listener
|
|
|
|
|
#
|
|
|
|
|
# @param broadcast = Method
|
|
|
|
|
##
|
|
|
|
|
def stop_listening(self):
|
|
|
|
|
self.listen = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
|
# Send a single request to Snapcast
|
|
|
|
|
#
|
|
|
|
|
# @param data = Dict
|
|
|
|
|
##
|
|
|
|
|
def request(self, data):
|
|
|
|
|
|
|
|
|
|
# No socket to make request on
|
2018-11-04 14:41:08 +13:00
|
|
|
if self.sock == None:
|
2018-11-03 23:19:03 +13:00
|
|
|
raise Exception("Socket not established");
|
|
|
|
|
|
|
|
|
|
# Construct our request, based on the provided data
|
|
|
|
|
request = {
|
|
|
|
|
'id': self.generateGuid(),
|
|
|
|
|
'jsonrpc': '2.0',
|
|
|
|
|
'method': data['method'],
|
|
|
|
|
'params': data['params'] if 'params' in data else {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.debug("Outgoing Snapcast message")
|
|
|
|
|
logger.debug(request)
|
|
|
|
|
|
|
|
|
|
# Convert to string. For some really nuts reason we need an extra trailing curly brace...
|
|
|
|
|
request = json.dumps(request)+'}'
|
|
|
|
|
|
|
|
|
|
# Attempt to send the request
|
|
|
|
|
try:
|
2018-11-04 14:41:08 +13:00
|
|
|
self.sock.send(request.encode('ascii')+b"\n")
|
2018-11-03 23:19:03 +13:00
|
|
|
|
|
|
|
|
except socket.error, e:
|
|
|
|
|
logger.error("Iris could not send request to Snapcast: %s" % e)
|
|
|
|
|
|
|
|
|
|
socket_response = {
|
|
|
|
|
'error': {
|
|
|
|
|
'message': "Failed to send request to Snapcast",
|
|
|
|
|
'data': str(e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Wait for response
|
|
|
|
|
while True:
|
|
|
|
|
|
|
|
|
|
try:
|
2018-11-04 14:41:08 +13:00
|
|
|
response = self.sock.recv(8192)
|
2018-11-03 23:19:03 +13:00
|
|
|
|
|
|
|
|
except socket.error, e:
|
|
|
|
|
logger.error("Iris failed to receive Snapcast response: %s" % e)
|
|
|
|
|
socket_response = {
|
|
|
|
|
'error': {
|
|
|
|
|
'message': "Failed to receive Snapcast response",
|
|
|
|
|
'data': str(e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if not len(response):
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
response = json.loads(response)
|
|
|
|
|
logger.debug("Incoming Snapcast message")
|
|
|
|
|
logger.debug(response)
|
|
|
|
|
|
|
|
|
|
if 'result' in response:
|
2018-11-04 14:41:08 +13:00
|
|
|
socket_response = response['result']
|
2018-11-03 23:19:03 +13:00
|
|
|
else:
|
2018-11-04 14:41:08 +13:00
|
|
|
socket_response = response['error']
|
2018-11-03 23:19:03 +13:00
|
|
|
|
|
|
|
|
except:
|
|
|
|
|
logger.error("Iris received malformed Snapcast response: "+response)
|
|
|
|
|
socket_response = {
|
|
|
|
|
'error': {
|
|
|
|
|
'message': "Malformed Snapcast response",
|
|
|
|
|
'data': response
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return socket_response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
|
# Generate a random string
|
|
|
|
|
#
|
|
|
|
|
# Used for connection_ids where none is provided by client
|
|
|
|
|
# @return string
|
|
|
|
|
##
|
|
|
|
|
def generateGuid(self):
|
|
|
|
|
length = 12
|
|
|
|
|
return ''.join(random.choice(string.lowercase) for i in range(length))
|
|
|
|
|
|