diff --git a/platypush/backend/http/static/css/music.snapcast.css b/platypush/backend/http/static/css/music.snapcast.css index f9351ba5..f981db90 100644 --- a/platypush/backend/http/static/css/music.snapcast.css +++ b/platypush/backend/http/static/css/music.snapcast.css @@ -1,5 +1,6 @@ .snapcast-host-container { min-width: 40em; + max-width: 80em; margin: 1em auto; background: rgba(245,245,245,0.6); border: 1px solid rgba(220,220,220,1.0); @@ -9,26 +10,113 @@ .snapcast-host-header { border-bottom: 1px solid rgba(220,220,220,1.0); font-size: 1em; + text-transform: uppercase; + padding: 0 .5em; } + .snapcast-host-header h1 { + font-size: 1.9em; + } + .snapcast-group-header { - padding: 0.5em; + padding: .5em; + margin: 0 1.4em 1.8em .2em; + border-bottom: 1px solid #e8e8e8; } - .snapcast-group-settings { - text-align: center; + .snapcast-settings-btn { cursor: pointer; + padding: .2em; } + .snapcast-settings-btn:hover { + border: .05em solid #e0e0e0; + padding: .15em; + } + .snapcast-group-header h2 { font-size: 1.5em; + margin-top: .6em; } -.snapcast-client-container { - padding: 0 1.4em; +.snapcast-client-disconnected { + 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; } +.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; + } + diff --git a/platypush/backend/http/static/js/music.snapcast.js b/platypush/backend/http/static/js/music.snapcast.js index 2c5e3f0c..d87de155 100644 --- a/platypush/backend/http/static/js/music.snapcast.js +++ b/platypush/backend/http/static/js/music.snapcast.js @@ -1,5 +1,6 @@ $(document).ready(function() { - var statuses = [], + var serverInfo = {}, + clientInfo = {}, $container = $('#snapcast-container'); var createPowerToggleElement = function(data) { @@ -25,19 +26,64 @@ $(document).ready(function() { 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) { $container.html(''); + serverInfo = {}; + clientInfo = {}; - var networkNames = Object.keys(window.config.snapcast_hosts); - for (var i=0; i < networkNames.length; i++) { + var hosts = Object.keys(window.config.snapcast_hosts); + + for (var i=0; i < hosts.length; i++) { var status = statuses[i]; - var networkName = networkNames[i]; + var host = hosts[i]; var name = status.server.host.name || status.server.host.ip; + serverInfo[host] = status.server; var $host = $('
') .addClass('snapcast-host-container') .data('name', name) - .data('network-name', networkName); + .data('host', host); var $header = $('
').addClass('row') .addClass('snapcast-host-header'); @@ -58,21 +104,25 @@ $(document).ready(function() { .addClass('snapcast-group-header'); var $groupTitle = $('

') - .addClass('ten columns'); + .addClass('snapcast-group-settings') + .addClass('snapcast-settings-btn') + .attr('data-modal', '#snapcast-group-modal') + .addClass('eleven columns'); var $groupSettings = $('') - .addClass('snapcast-group-settings') - .addClass('fa fa-cog') - .data('name', groupName) - .data('id', group.id); + .attr('data-modal', '#snapcast-group-modal') + .addClass('fa fa-ellipsis-v'); var $groupName = $('') + .attr('data-modal', '#snapcast-group-modal') .html('  ' + groupName); var $groupMuteToggle = createPowerToggleElement({ id: group.id, 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); $groupName.appendTo($groupTitle); @@ -82,22 +132,41 @@ $(document).ready(function() { for (var client of group.clients) { 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 = $('
') .addClass('snapcast-client-container') .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 = $('
').addClass('row') - .addClass('snapcast-client-header'); + .addClass('snapcast-client-row'); var $clientTitle = $('

') - .addClass('three columns') - .data('connected', client.connected) - .text(clientName); + .addClass('snapcast-settings-btn') + .addClass('snapcast-client-settings') + .attr('data-modal', '#snapcast-client-modal') + .addClass('two columns'); + + var $clientSettings = $('') + .attr('data-modal', '#snapcast-client-modal') + .addClass('fa fa-ellipsis-v'); + + var $clientName = $('') + .attr('data-modal', '#snapcast-client-modal') + .html('  ' + clientName); var $volumeSlider = $('') .addClass('slider snapcast-volume-slider') - .addClass('eight columns') + .addClass('nine columns') .data('id', client.id) .attr('type', 'range') .attr('min', 0).attr('max', 100) @@ -108,8 +177,11 @@ $(document).ready(function() { on: !client.config.volume.muted, }) .addClass('one column') + .addClass('snapcast-mute-toggle') .addClass('snapcast-client-mute-toggle'); + $clientSettings.appendTo($clientTitle); + $clientName.appendTo($clientTitle); $clientTitle.appendTo($clientRow); $volumeSlider.appendTo($clientRow); $clientMuteToggle.appendTo($clientRow); @@ -142,7 +214,7 @@ $(document).ready(function() { $.when.apply($, promises) .done(function() { - statuses = []; + var statuses = []; for (var status of arguments) { statuses.push(status[0].response.output); } @@ -154,168 +226,146 @@ $(document).ready(function() { }; var initBindings = function() { - // $roomsList.on('click touch', '.room-item', function() { - // $('.room-item').removeClass('selected'); - // $('.room-lights-item').hide(); - // $('.room-scenes-item').hide(); + $container.on('click touch', '.toggle--checkbox', function(evt) { + evt.stopPropagation(); + var id = $(this).attr('id'); + 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'); - // var $roomLights = $('.room-lights-item').filter(function(i, item) { - // return $(item).data('id') === roomId - // }); + if ($(this).parents('.snapcast-mute-toggle').hasClass('snapcast-client-mute-toggle')) { + args.client = id; + } 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) { - // return $(item).data('room-id') === roomId - // }); + execute({ + type: 'request', + action: 'music.snapcast.mute', + args: args, + }); + }); - // $(this).addClass('selected'); - // $roomLights.show(); - // $roomScenes.show(); - // }); + $container.on('mouseup touchend', '.snapcast-volume-slider', function(evt) { + evt.stopPropagation(); + 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() { - // $('.scene-item').removeClass('selected'); - // $(this).addClass('selected'); + execute({ + type: 'request', + action: 'music.snapcast.volume', + args: args, + }); + }); - // execute({ - // type: 'request', - // action: 'light.hue.scene', - // args: { - // name: $(this).data('name') - // } - // }, refreshStatus); - // }); + $container.on('click touch', '.snapcast-group-settings', function(evt) { + var host = $(this).parents('.snapcast-host-container').data('host'); + var groupId = $(this).parents('.snapcast-group-container').data('id'); + var groupName = $(this).parents('.snapcast-group-container').data('name'); + var $modal = $($(this).data('modal')); - // $lightsList.on('click touch', '.light-item-name', function() { - // var $lightItem = $(this).parents('.light-item'); - // var $colorSelector = $lightItem.find('.light-color-selector'); + $modal.find('.modal-header').text(groupName); + }); - // $('.light-color-selector').hide(); - // $colorSelector.toggle(); + $container.on('click touch', '.snapcast-client-settings', function(evt) { + 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'); - // $lightItem.addClass('selected'); - // }); + $form.data('host', host); + $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) { - // e.stopPropagation(); + for (var attr in info) { + $info.find('[data-bind=' + attr + ']').text(info[attr]); + } + }); - // var $lightItem = $($(this).parents('.light-item')); - // var type = $lightItem.data('type'); - // 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: {} - // }; + $('.snapcast-form').on('click touch', '[data-dismiss-modal]', function(evt) { + var $modal = $(this).parents($(this).data('dismiss-modal')); - // args['args'][key] = [name]; - // execute(args, function() { - // $lightItem.data('on', !isOn); - // refreshStatus(); - // }); - // }); + var clearModal = function() { + $modal.find('form').find('input').prop('disabled', false); + $modal.find('form').find('[name=delete]').prop('checked', false); + }; - // $lightsList.on('click touch', '.animation-switch', function(e) { - // e.stopPropagation(); + console.log($modal); + clearModal(); + }); - // var turnedOn = $(this).prop('checked'); - // var args = {}; - // args['groups'] = $(this).parents('.animation-item').data('name'); - // args['animation'] = $(this).parents('.animation-item') - // .find('input.animation-type:checked').data('type'); + $('#snapcast-client-form').on('submit', function(evt) { + var $form = $(this); + var host = $form.data('host'); + var clientId = $form.data('client'); - // var $animationCtrl = $(this).parents('.animation-item') - // .find('.animation-container').filter( - // (index, node) => $(node).data('animation-type') === args['animation'] - // ); + var clearModal = function() { + $form.parents('.modal').fadeOut(); + $form.find('input').prop('disabled', false); + $form.find('[name=delete]').prop('checked', false); + }; - // var params = $animationCtrl.find('*').filter( - // (index, node) => $(node).data('animation-property')) - // .toArray().reduce( - // (map, input) => { - // if ($(input).val().length) { - // var val = $(input).val(); - // val = Array.isArray(val) ? val.map((i) => parseFloat(i)) : parseFloat(val); - // map[$(input).data('animation-property')] = val; - // } + var request = { + type: 'request', + args: { + host: host, + port: window.config.snapcast_hosts[host], + client: clientId, + }, + }; - // 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)) { - // args[p] = params[p]; - // } + request.action = 'music.snapcast.delete_client'; + } else { + request.action = 'music.snapcast.set_client_name'; + request.args.name = $form.find('input[name=name]').val().trim(); + } - // if (turnedOn) { - // execute( - // { - // type: 'request', - // action: 'light.hue.animate', - // args: args, - // }, + $form.find('input').prop('disabled', true); - // onSuccess = function() { - // $(this).prop('checked', true); - // } - // ); - // } else { - // execute( - // { - // type: 'request', - // action: 'light.hue.stop_animation', - // }, + execute( + (response) => {}, + (xhr, status, error) => { + createNotification({ + 'icon': 'exclamation', + 'text': status + ': ' + error, + }); + }, + () => { + clearModal(); + } + ); - // onSuccess = function() { - // $(this).prop('checked', false); - // } - // ); - // } - // }); + return false; + }); + }; - // $lightsList.on('mouseup touchend', '.light-slider', function() { - // var property = $(this).data('property'); - // 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 initEvents = function() { + window.registerEventListener(onEvent); }; var init = function() { redraw(); + initEvents(); }; init(); diff --git a/platypush/backend/http/templates/plugins/music.snapcast.html b/platypush/backend/http/templates/plugins/music.snapcast.html index 9928b064..7b9105b0 100644 --- a/platypush/backend/http/templates/plugins/music.snapcast.html +++ b/platypush/backend/http/templates/plugins/music.snapcast.html @@ -13,6 +13,91 @@ } + + + +
diff --git a/platypush/backend/music/snapcast.py b/platypush/backend/music/snapcast.py index f274a583..2cc3d717 100644 --- a/platypush/backend/music/snapcast.py +++ b/platypush/backend/music/snapcast.py @@ -129,7 +129,20 @@ class MusicSnapcastBackend(Backend): def _client(self, host, port): def _thread(): - status = self._status(host, port) + status = None + + try: + 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(): try: