Connected the wires between Snapcast backend and frontend

This commit is contained in:
Fabio Manganiello 2019-01-08 01:47:16 +01:00
parent a346442246
commit fd8c3bb846
4 changed files with 398 additions and 162 deletions

View file

@ -1,5 +1,6 @@
.snapcast-host-container { .snapcast-host-container {
min-width: 40em; min-width: 40em;
max-width: 80em;
margin: 1em auto; margin: 1em auto;
background: rgba(245,245,245,0.6); background: rgba(245,245,245,0.6);
border: 1px solid rgba(220,220,220,1.0); border: 1px solid rgba(220,220,220,1.0);
@ -9,26 +10,113 @@
.snapcast-host-header { .snapcast-host-header {
border-bottom: 1px solid rgba(220,220,220,1.0); border-bottom: 1px solid rgba(220,220,220,1.0);
font-size: 1em; font-size: 1em;
text-transform: uppercase;
padding: 0 .5em;
} }
.snapcast-host-header h1 {
font-size: 1.9em;
}
.snapcast-group-header { .snapcast-group-header {
padding: 0.5em; padding: .5em;
margin: 0 1.4em 1.8em .2em;
border-bottom: 1px solid #e8e8e8;
} }
.snapcast-group-settings { .snapcast-settings-btn {
text-align: center;
cursor: pointer; cursor: pointer;
padding: .2em;
}
.snapcast-settings-btn:hover {
border: .05em solid #e0e0e0;
padding: .15em;
} }
.snapcast-group-header h2 { .snapcast-group-header h2 {
font-size: 1.5em; font-size: 1.5em;
margin-top: .6em;
} }
.snapcast-client-container { .snapcast-client-disconnected {
padding: 0 1.4em; color: rgba(0, 0, 0, 0.35);
} }
.snapcast-client-header h3 { .snapcast-client-row {
padding: 0 1.4em;
margin: 1.5em .5em;
}
.snapcast-client-row h3 {
font-size: 1.2em; font-size: 1.2em;
} }
.snapcast-client-mute-toggle {
margin-top: -1.2em;
}
.snapcast-form {
margin-bottom: 1rem;
}
.snapcast-form * > input[type=text] {
width: 100%;
}
.snapcast-form > .row {
padding: 0.5rem;
}
.snapcast-form-bottom {
text-align: right;
margin-top: 2rem;
border-top: 1px solid #ddd;
}
.snapcast-form-bottom input {
margin-top: 2rem;
}
.snapcast-form * > label {
transform: translateY(25%);
}
.snapcast-form > .snapcast-client-info {
max-width: 70%;
margin: 1em auto .5em auto;
padding: 2em;
background: rgba(240,240,240,0.6);
border-radius: 10px;
border: .05em solid rgba(225,225,225,1.0)
}
.snapcast-form > .snapcast-client-info > .row {
padding: .2em;
}
.snapcast-form > .snapcast-client-info > .row:hover {
background-color: #daf8e2 !important;
}
.snapcast-form > .snapcast-client-info * > .info-name {
font-weight: bold;
}
.snapcast-form > .snapcast-client-info * > .info-value {
text-align: right;
}
.snapcast-form > .snapcast-client-delete {
width: 30%;
margin: 1em auto 0 auto;
text-align: center;
}
.snapcast-form > .snapcast-client-delete > label {
display: inline;
margin-left: .5em;
color: rgba(200, 44, 23, 1.0);
text-align: right;
}

View file

@ -1,5 +1,6 @@
$(document).ready(function() { $(document).ready(function() {
var statuses = [], var serverInfo = {},
clientInfo = {},
$container = $('#snapcast-container'); $container = $('#snapcast-container');
var createPowerToggleElement = function(data) { var createPowerToggleElement = function(data) {
@ -25,19 +26,64 @@ $(document).ready(function() {
return $powerToggle; return $powerToggle;
}; };
var onEvent = function(event) {
switch (event.args.type) {
case 'platypush.message.event.music.snapcast.ClientConnectedEvent':
case 'platypush.message.event.music.snapcast.ClientDisconnectedEvent':
case 'platypush.message.event.music.snapcast.ClientNameChangeEvent':
case 'platypush.message.event.music.snapcast.GroupStreamChangeEvent':
case 'platypush.message.event.music.snapcast.ServerUpdateEvent':
redraw();
break;
case 'platypush.message.event.music.snapcast.ClientVolumeChangeEvent':
var $host = $($container.find('.snapcast-host-container').filter(
(i, hostDiv) => $(hostDiv).data('host') === event.args.host
));
var $client = $($host.find('.snapcast-client-container').filter(
(i, clientDiv) => $(clientDiv).data('id') === event.args.client
));
$client.find('.snapcast-volume-slider').val(event.args.volume);
$client.find('.snapcast-mute-toggle').find('input.toggle--checkbox')
.prop('checked', !event.args.muted);
break;
case 'platypush.message.event.music.snapcast.GroupMuteChangeEvent':
var $host = $($container.find('.snapcast-host-container').filter(
(i, hostDiv) => $(hostDiv).data('host') === event.args.host
));
var $group = $($host.find('.snapcast-group-container').filter(
(i, groupDiv) => $(groupDiv).data('id') === event.args.group
));
$group.find('.snapcast-group-mute-toggle').find('input.toggle--checkbox')
.prop('checked', !event.args.muted);
break;
}
};
var update = function(statuses) { var update = function(statuses) {
$container.html(''); $container.html('');
serverInfo = {};
clientInfo = {};
var networkNames = Object.keys(window.config.snapcast_hosts); var hosts = Object.keys(window.config.snapcast_hosts);
for (var i=0; i < networkNames.length; i++) {
for (var i=0; i < hosts.length; i++) {
var status = statuses[i]; var status = statuses[i];
var networkName = networkNames[i]; var host = hosts[i];
var name = status.server.host.name || status.server.host.ip; var name = status.server.host.name || status.server.host.ip;
serverInfo[host] = status.server;
var $host = $('<div></div>') var $host = $('<div></div>')
.addClass('snapcast-host-container') .addClass('snapcast-host-container')
.data('name', name) .data('name', name)
.data('network-name', networkName); .data('host', host);
var $header = $('<div></div>').addClass('row') var $header = $('<div></div>').addClass('row')
.addClass('snapcast-host-header'); .addClass('snapcast-host-header');
@ -58,21 +104,25 @@ $(document).ready(function() {
.addClass('snapcast-group-header'); .addClass('snapcast-group-header');
var $groupTitle = $('<h2></h2>') var $groupTitle = $('<h2></h2>')
.addClass('ten columns'); .addClass('snapcast-group-settings')
.addClass('snapcast-settings-btn')
.attr('data-modal', '#snapcast-group-modal')
.addClass('eleven columns');
var $groupSettings = $('<i></i>') var $groupSettings = $('<i></i>')
.addClass('snapcast-group-settings') .attr('data-modal', '#snapcast-group-modal')
.addClass('fa fa-cog') .addClass('fa fa-ellipsis-v');
.data('name', groupName)
.data('id', group.id);
var $groupName = $('<span></span>') var $groupName = $('<span></span>')
.attr('data-modal', '#snapcast-group-modal')
.html('&nbsp; ' + groupName); .html('&nbsp; ' + groupName);
var $groupMuteToggle = createPowerToggleElement({ var $groupMuteToggle = createPowerToggleElement({
id: group.id, id: group.id,
on: !group.muted, on: !group.muted,
}).addClass('two columns').addClass('snapcast-group-mute-toggle'); }).addClass('one column')
.addClass('snapcast-mute-toggle')
.addClass('snapcast-group-mute-toggle');
$groupSettings.appendTo($groupTitle); $groupSettings.appendTo($groupTitle);
$groupName.appendTo($groupTitle); $groupName.appendTo($groupTitle);
@ -82,22 +132,41 @@ $(document).ready(function() {
for (var client of group.clients) { for (var client of group.clients) {
var clientName = client.config.name || client.host.name || client.host.ip; var clientName = client.config.name || client.host.name || client.host.ip;
clientInfo[client.id] = client.host;
clientInfo[client.id].clientName = client.snapclient.name;
clientInfo[client.id].clientVersion = client.snapclient.version;
clientInfo[client.id].protocolVersion = client.snapclient.protocolVersion;
var $client = $('<div></div>') var $client = $('<div></div>')
.addClass('snapcast-client-container') .addClass('snapcast-client-container')
.data('name', clientName) .data('name', clientName)
.data('id', client.id); .data('id', client.id)
.data('connected', client.connected);
if (!client.connected) {
$client.addClass('snapcast-client-disconnected');
}
var $clientRow = $('<div></div>').addClass('row') var $clientRow = $('<div></div>').addClass('row')
.addClass('snapcast-client-header'); .addClass('snapcast-client-row');
var $clientTitle = $('<h3></h3>') var $clientTitle = $('<h3></h3>')
.addClass('three columns') .addClass('snapcast-settings-btn')
.data('connected', client.connected) .addClass('snapcast-client-settings')
.text(clientName); .attr('data-modal', '#snapcast-client-modal')
.addClass('two columns');
var $clientSettings = $('<i></i>')
.attr('data-modal', '#snapcast-client-modal')
.addClass('fa fa-ellipsis-v');
var $clientName = $('<span></span>')
.attr('data-modal', '#snapcast-client-modal')
.html('&nbsp; ' + clientName);
var $volumeSlider = $('<input></input>') var $volumeSlider = $('<input></input>')
.addClass('slider snapcast-volume-slider') .addClass('slider snapcast-volume-slider')
.addClass('eight columns') .addClass('nine columns')
.data('id', client.id) .data('id', client.id)
.attr('type', 'range') .attr('type', 'range')
.attr('min', 0).attr('max', 100) .attr('min', 0).attr('max', 100)
@ -108,8 +177,11 @@ $(document).ready(function() {
on: !client.config.volume.muted, on: !client.config.volume.muted,
}) })
.addClass('one column') .addClass('one column')
.addClass('snapcast-mute-toggle')
.addClass('snapcast-client-mute-toggle'); .addClass('snapcast-client-mute-toggle');
$clientSettings.appendTo($clientTitle);
$clientName.appendTo($clientTitle);
$clientTitle.appendTo($clientRow); $clientTitle.appendTo($clientRow);
$volumeSlider.appendTo($clientRow); $volumeSlider.appendTo($clientRow);
$clientMuteToggle.appendTo($clientRow); $clientMuteToggle.appendTo($clientRow);
@ -142,7 +214,7 @@ $(document).ready(function() {
$.when.apply($, promises) $.when.apply($, promises)
.done(function() { .done(function() {
statuses = []; var statuses = [];
for (var status of arguments) { for (var status of arguments) {
statuses.push(status[0].response.output); statuses.push(status[0].response.output);
} }
@ -154,168 +226,146 @@ $(document).ready(function() {
}; };
var initBindings = function() { var initBindings = function() {
// $roomsList.on('click touch', '.room-item', function() { $container.on('click touch', '.toggle--checkbox', function(evt) {
// $('.room-item').removeClass('selected'); evt.stopPropagation();
// $('.room-lights-item').hide(); var id = $(this).attr('id');
// $('.room-scenes-item').hide(); var host = $(this).parents('.snapcast-host-container').data('host');
var args = {
host: host,
port: window.config.snapcast_hosts[host],
mute: !$(this).prop('checked'),
};
// var roomId = $(this).data('id'); if ($(this).parents('.snapcast-mute-toggle').hasClass('snapcast-client-mute-toggle')) {
// var $roomLights = $('.room-lights-item').filter(function(i, item) { args.client = id;
// return $(item).data('id') === roomId } else if ($(this).parents('.snapcast-mute-toggle').hasClass('snapcast-group-mute-toggle')) {
// }); args.group = id;
} else {
return;
}
// var $roomScenes = $('.room-scenes-item').filter(function(i, item) { execute({
// return $(item).data('room-id') === roomId type: 'request',
// }); action: 'music.snapcast.mute',
args: args,
});
});
// $(this).addClass('selected'); $container.on('mouseup touchend', '.snapcast-volume-slider', function(evt) {
// $roomLights.show(); evt.stopPropagation();
// $roomScenes.show(); var id = $(this).data('id');
// }); var host = $(this).parents('.snapcast-host-container').data('host');
var args = {
host: host,
port: window.config.snapcast_hosts[host],
client: id,
volume: $(this).val(),
};
// $scenesList.on('click touch', '.scene-item', function() { execute({
// $('.scene-item').removeClass('selected'); type: 'request',
// $(this).addClass('selected'); action: 'music.snapcast.volume',
args: args,
});
});
// execute({ $container.on('click touch', '.snapcast-group-settings', function(evt) {
// type: 'request', var host = $(this).parents('.snapcast-host-container').data('host');
// action: 'light.hue.scene', var groupId = $(this).parents('.snapcast-group-container').data('id');
// args: { var groupName = $(this).parents('.snapcast-group-container').data('name');
// name: $(this).data('name') var $modal = $($(this).data('modal'));
// }
// }, refreshStatus);
// });
// $lightsList.on('click touch', '.light-item-name', function() { $modal.find('.modal-header').text(groupName);
// var $lightItem = $(this).parents('.light-item'); });
// var $colorSelector = $lightItem.find('.light-color-selector');
// $('.light-color-selector').hide(); $container.on('click touch', '.snapcast-client-settings', function(evt) {
// $colorSelector.toggle(); var host = $(this).parents('.snapcast-host-container').data('host');
var clientId = $(this).parents('.snapcast-client-container').data('id');
var clientName = $(this).parents('.snapcast-client-container').data('name');
var info = clientInfo[clientId];
var $modal = $($(this).data('modal'));
var $form = $modal.find('#snapcast-client-form');
var $info = $form.find('.snapcast-client-info');
// $('.light-item').removeClass('selected'); $form.data('host', host);
// $lightItem.addClass('selected'); $form.data('client', clientId);
// }); $modal.find('.modal-header').text(clientName);
$form.find('input[name=name]').val(clientName);
// $lightsList.on('click touch', '.light-ctrl-switch', function(e) { for (var attr in info) {
// e.stopPropagation(); $info.find('[data-bind=' + attr + ']').text(info[attr]);
}
});
// var $lightItem = $($(this).parents('.light-item')); $('.snapcast-form').on('click touch', '[data-dismiss-modal]', function(evt) {
// var type = $lightItem.data('type'); var $modal = $(this).parents($(this).data('dismiss-modal'));
// var name = $lightItem.data('name');
// var isOn = $lightItem.data('on');
// var action = 'light.hue.' + (isOn ? 'off' : 'on');
// var key = (type == 'light' ? 'lights' : 'groups');
// var args = {
// type: 'request',
// action: action,
// args: {}
// };
// args['args'][key] = [name]; var clearModal = function() {
// execute(args, function() { $modal.find('form').find('input').prop('disabled', false);
// $lightItem.data('on', !isOn); $modal.find('form').find('[name=delete]').prop('checked', false);
// refreshStatus(); };
// });
// });
// $lightsList.on('click touch', '.animation-switch', function(e) { console.log($modal);
// e.stopPropagation(); clearModal();
});
// var turnedOn = $(this).prop('checked'); $('#snapcast-client-form').on('submit', function(evt) {
// var args = {}; var $form = $(this);
// args['groups'] = $(this).parents('.animation-item').data('name'); var host = $form.data('host');
// args['animation'] = $(this).parents('.animation-item') var clientId = $form.data('client');
// .find('input.animation-type:checked').data('type');
// var $animationCtrl = $(this).parents('.animation-item') var clearModal = function() {
// .find('.animation-container').filter( $form.parents('.modal').fadeOut();
// (index, node) => $(node).data('animation-type') === args['animation'] $form.find('input').prop('disabled', false);
// ); $form.find('[name=delete]').prop('checked', false);
};
// var params = $animationCtrl.find('*').filter( var request = {
// (index, node) => $(node).data('animation-property')) type: 'request',
// .toArray().reduce( args: {
// (map, input) => { host: host,
// if ($(input).val().length) { port: window.config.snapcast_hosts[host],
// var val = $(input).val(); client: clientId,
// val = Array.isArray(val) ? val.map((i) => parseFloat(i)) : parseFloat(val); },
// map[$(input).data('animation-property')] = val; };
// }
// return map if ($form.find('[name=delete]').prop('checked')) {
// }, {} if (!confirm('Are you sure you want to remove this client?')) {
// ); return false;
}
// for (var p of Object.keys(params)) { request.action = 'music.snapcast.delete_client';
// args[p] = params[p]; } else {
// } request.action = 'music.snapcast.set_client_name';
request.args.name = $form.find('input[name=name]').val().trim();
}
// if (turnedOn) { $form.find('input').prop('disabled', true);
// execute(
// {
// type: 'request',
// action: 'light.hue.animate',
// args: args,
// },
// onSuccess = function() { execute(
// $(this).prop('checked', true); (response) => {},
// } (xhr, status, error) => {
// ); createNotification({
// } else { 'icon': 'exclamation',
// execute( 'text': status + ': ' + error,
// { });
// type: 'request', },
// action: 'light.hue.stop_animation', () => {
// }, clearModal();
}
);
// onSuccess = function() { return false;
// $(this).prop('checked', false); });
// } };
// );
// }
// });
// $lightsList.on('mouseup touchend', '.light-slider', function() { var initEvents = function() {
// var property = $(this).data('property'); window.registerEventListener(onEvent);
// var type = $(this).data('type');
// var name = $(this).data('name');
// var args = {
// type: 'request',
// action: 'light.hue.' + property,
// args: { value: $(this).val() }
// };
// if (type === 'light') {
// args.args.lights = [name];
// } else {
// args.args.groups = [name];
// }
// execute(args, refreshStatus);
// });
// $lightsList.on('click touch', 'input.animation-type', function(e) {
// var type = $(this).data('type');
// var $animationContainers = $(this).parents('.animation-item').find('.animation-container')
// var $animationContainer = $(this).parents('.animation-item').find('.animation-container')
// .filter(function() { return $(this).data('animationType') === type })
// $animationContainers.hide();
// $animationContainer.show();
// });
// if (window.config.light.hue.default_group) {
// var $defaultRoomItem = $roomsList.find('.room-item').filter(
// (i, r) => $(r).data('name') == window.config.light.hue.default_group);
// $defaultRoomItem.click();
// }
}; };
var init = function() { var init = function() {
redraw(); redraw();
initEvents();
}; };
init(); init();

View file

@ -13,6 +13,91 @@
} }
</script> </script>
<div id="snapcast-group-modal" class="modal snapcast-modal">
<div class="modal-container">
<div class="modal-header"></div>
<div class="modal-body">
<form id="snapcast-group-form" class="snapcast-form" action="#">
<div class="row snapcast-form-bottom">
<div class="six columns offset-by-six forms-btns">
<input type="button" class="btn-default" data-dismiss-modal="#snapcast-group-modal"
value="Close">
<input type="submit" class="btn-primary" value="Save">
</div>
</div>
</form>
</div>
</div>
</div>
<div id="snapcast-client-modal" class="modal snapcast-modal">
<div class="modal-container">
<div class="modal-header"></div>
<div class="modal-body">
<form id="snapcast-client-form" class="snapcast-form" action="#">
<div class="row form-row">
<div class="two columns">
<label for="name">Name</label>
</div>
<div class="ten columns">
<input type="text" name="name">
</div>
</div>
<div class="row snapcast-client-info">
<div class="row">
<div class="three columns info-name">IP Address</div>
<div class="nine columns info-value" data-bind="ip"></div>
</div>
<div class="row">
<div class="three columns info-name">MAC Address</div>
<div class="nine columns info-value" data-bind="mac"></div>
</div>
<div class="row">
<div class="three columns info-name">OS</div>
<div class="nine columns info-value" data-bind="os"></div>
</div>
<div class="row">
<div class="three columns info-name">Architecture</div>
<div class="nine columns info-value" data-bind="arch"></div>
</div>
<div class="row">
<div class="three columns info-name">Client Name</div>
<div class="nine columns info-value" data-bind="clientName"></div>
</div>
<div class="row">
<div class="three columns info-name">Client Version</div>
<div class="nine columns info-value" data-bind="clientVersion"></div>
</div>
<div class="row">
<div class="three columns info-name">Protocol Version</div>
<div class="nine columns info-value" data-bind="protocolVersion"></div>
</div>
</div>
<div class="row snapcast-client-delete">
<input type="checkbox" name="delete">
<label for="delete">Delete client</label>
</div>
<div class="row snapcast-form-bottom">
<div class="six columns offset-by-six forms-btns">
<input type="button" class="btn-default" data-dismiss-modal="#snapcast-client-modal"
value="Close">
<input type="submit" class="btn-primary" value="Save">
</div>
</div>
</form>
</div>
</div>
</div>
<div id="snapcast-container" class="row"> <div id="snapcast-container" class="row">
</div> </div>

View file

@ -129,7 +129,20 @@ class MusicSnapcastBackend(Backend):
def _client(self, host, port): def _client(self, host, port):
def _thread(): def _thread():
status = None
try:
status = self._status(host, port) status = self._status(host, port)
except Exception as e:
self.logger.warning(('Exception while getting the status ' +
'of the Snapcast server {}:{}: {}').
format(host, port, str(e)))
try:
self._disconnect(host, port)
time.sleep(5)
except:
pass
while not self.should_stop(): while not self.should_stop():
try: try: