Finalized new Snapcast webpanel plugin

This commit is contained in:
Fabio Manganiello 2019-06-10 15:11:24 +02:00
parent 95a9c22618
commit 91ef6f3ce2
12 changed files with 382 additions and 100 deletions

View file

@ -1,5 +1,5 @@
.modal-container { .modal-container {
position: absolute; position: fixed;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View file

@ -70,11 +70,58 @@ $host-shadow: $default-shadow;
.modal { .modal {
.info { .info {
.row { padding: 2rem;
&:hover { background: $hover-bg; }
.value { .section {
text-align: right; border: $default-border-2;
border-radius: 1rem;
&:not(last-child) { margin-bottom: 2rem; }
.title {
border-bottom: $default-border-2;
padding: 1rem;
text-transform: uppercase;
} }
&.clients .row {
padding: .5rem;
label {
margin: 0 0 0 .5rem;
}
}
}
.row {
padding: .33rem .5rem;
border-radius: .75rem;
display: flex;
align-items: center;
&:nth-child(odd) { background: rgba(255, 255, 255, 0.0); }
&:nth-child(even) { background: $default-bg-3; }
&:hover { background: $hover-bg; }
.label { font-weight: bold; }
.value { text-align: right; }
}
}
}
}
#music-snapcast-client-info {
.info {
.buttons {
background: initial;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: $default-border-2;
display: flex;
justify-content: center;
button {
color: #900;
border-color: #900;
} }
} }
} }
@ -99,7 +146,7 @@ $host-shadow: $default-shadow;
@media #{map-get($widths, 'l')} { @media #{map-get($widths, 'l')} {
.music-snapcast-container { .music-snapcast-container {
.modal { .modal {
width: 65vw; width: 45vw;
} }
} }
} }

View file

@ -32,6 +32,35 @@ Vue.component('music-snapcast-client', {
}); });
Vue.component('music-snapcast-client-info', { Vue.component('music-snapcast-client-info', {
props: ['info'], props: {
info: { type: Object }
},
data: function() {
return {
loading: false,
};
},
methods: {
deleteClient: async function(event) {
if (!confirm('Are you SURE that you want to remove this client?')) {
return;
}
this.loading = true;
await request('music.snapcast.delete_client', {
client: this.info.id,
host: this.info.server.host.name,
port: this.info.server.host.port,
});
this.loading = false;
createNotification({
text: 'Snapcast client successfully removed',
image: { icon: 'check' }
});
},
},
}); });

View file

@ -22,6 +22,59 @@ Vue.component('music-snapcast-group', {
}); });
Vue.component('music-snapcast-group-info', { Vue.component('music-snapcast-group-info', {
props: ['info'], props: {
info: { type: Object }
},
data: function() {
return {
loading: false,
};
},
methods: {
onClientUpdate: async function(event) {
var clients = this.$refs.groupClients
.map(row => row.querySelector('input[type=checkbox]:checked'))
.filter(_ => _ != null)
.map(input => input.value);
this.loading = true;
await request('music.snapcast.group_set_clients', {
clients: clients,
group: this.info.group.id,
host: this.info.server.host.name,
port: this.info.server.host.port,
});
this.loading = false;
createNotification({
text: 'Snapcast group successfully updated',
image: {
icon: 'check',
}
});
},
onStreamUpdate: async function(event) {
this.loading = true;
await request('music.snapcast.group_set_stream', {
stream_id: event.target.value,
group: this.info.group.id,
host: this.info.server.host.name,
port: this.info.server.host.port,
});
this.loading = false;
this.info.group.stream_id = event.target.value;
createNotification({
text: 'Snapcast stream successfully updated',
image: {
icon: 'check',
}
});
},
},
}); });

View file

@ -15,6 +15,8 @@ Vue.component('music-snapcast-host', {
}); });
Vue.component('music-snapcast-host-info', { Vue.component('music-snapcast-host-info', {
props: ['info'], props: {
info: { type: Object }
},
}); });

View file

@ -4,6 +4,7 @@ Vue.component('music-snapcast', {
data: function() { data: function() {
return { return {
hosts: {}, hosts: {},
ports: {},
modal: { modal: {
host: { host: {
visible: false, visible: false,
@ -24,6 +25,31 @@ Vue.component('music-snapcast', {
}, },
methods: { methods: {
_parseServerStatus: function(status) {
status.server.host.port = this.ports[status.server.host.name];
var groups = {};
for (const group of status.groups) {
var clients = {};
for (const client of group.clients) {
clients[client.id] = client;
}
group.clients = clients;
groups[group.id] = group;
}
status.groups = groups;
var streams = {};
for (const stream of status.streams) {
streams[stream.id] = stream;
}
status.streams = streams;
Vue.set(this.hosts, status.server.host.name, status);
},
refresh: async function() { refresh: async function() {
let hosts = await request('music.snapcast.get_backend_hosts'); let hosts = await request('music.snapcast.get_backend_hosts');
let promises = Object.keys(hosts).map( let promises = Object.keys(hosts).map(
@ -34,28 +60,8 @@ Vue.component('music-snapcast', {
this.hosts = {}; this.hosts = {};
for (const status of statuses) { for (const status of statuses) {
status.server.host.port = hosts[status.server.host.name]; this.ports[status.server.host.name] = hosts[status.server.host.name];
var groups = {}; this._parseServerStatus(status);
for (const group of status.groups) {
var clients = {};
for (const client of group.clients) {
clients[client.id] = client;
}
group.clients = clients;
groups[group.id] = group;
}
status.groups = groups;
var streams = {};
for (const stream of status.streams) {
streams[stream.id] = stream;
}
status.streams = streams;
Vue.set(this.hosts, status.server.host.name, status);
} }
}, },
@ -72,11 +78,11 @@ Vue.component('music-snapcast', {
}, },
onServerUpdate: function(event) { onServerUpdate: function(event) {
this.refresh(); this._parseServerStatus(event.server);
}, },
onStreamUpdate: function(event) { onStreamUpdate: function(event) {
this.streams[event.stream_id] = event.stream; this.hosts[event.host].streams[event.stream.id] = event.stream;
}, },
onClientVolumeChange: function(event) { onClientVolumeChange: function(event) {
@ -103,10 +109,21 @@ Vue.component('music-snapcast', {
this.modal[event.type].info = this.hosts[event.host]; this.modal[event.type].info = this.hosts[event.host];
break; break;
case 'group': case 'group':
this.modal[event.type].info = this.hosts[event.host].groups[event.group]; this.modal[event.type].info.server = this.hosts[event.host].server;
this.modal[event.type].info.group = this.hosts[event.host].groups[event.group];
this.modal[event.type].info.streams = this.hosts[event.host].streams;
this.modal[event.type].info.clients = {};
for (const group of Object.values(this.hosts[event.host].groups)) {
for (const client of Object.values(group.clients)) {
this.modal[event.type].info.clients[client.id] = client;
}
}
break; break;
case 'client': case 'client':
this.modal[event.type].info = this.hosts[event.host].groups[event.group].clients[event.client]; this.modal[event.type].info = this.hosts[event.host].groups[event.group].clients[event.client];
this.modal[event.type].info.server = this.hosts[event.host].server;
break; break;
} }

View file

@ -5,7 +5,7 @@
<div class="head"> <div class="head">
<div class="col-10 name" <div class="col-10 name"
@click="bus.$emit('modal-show', {type:'group', group:id, host:server.name})"> @click="bus.$emit('modal-show', {type:'group', group:id, host:server.name})">
<i class="icon fa fa-network-wired"></i> <i class="icon fa" :class="{'fa-play': stream.status === 'playing', 'fa-stop': stream.status !== 'playing'}"></i>
{% raw %}{{ name || stream.id || id }}{% endraw %} {% raw %}{{ name || stream.id || id }}{% endraw %}
</div> </div>
<div class="col-2 switch pull-right"> <div class="col-2 switch pull-right">

View file

@ -3,71 +3,15 @@
<script type="text/x-template" id="tmpl-music-snapcast"> <script type="text/x-template" id="tmpl-music-snapcast">
<div class="row music-snapcast-container"> <div class="row music-snapcast-container">
<modal id="music-snapcast-host-info" title="Server info" v-model="modal.host.visible" ref="modalHost"> <modal id="music-snapcast-host-info" title="Server info" v-model="modal.host.visible" ref="modalHost">
<music-snapcast-host-info v-if="modal.host.visible" :info="modal.host.info" inline-template> {% include 'plugins/music.snapcast/modals/host.html' %}
<div class="info">
<div class="row" v-if="info.server.host.ip && modal.host.info.server.host.ip.length">
<div class="label col-3">IP Address</div>
<div class="value col-9" v-text="modal.host.info.server.host.ip"></div>
</div>
<div class="row" v-if="info.server.host.mac && info.server.host.mac.length">
<div class="label col-3">MAC Address</div>
<div class="value col-9" v-text="info.server.host.mac"></div>
</div>
<div class="row" v-if="info.server.host.name && info.server.host.name.length">
<div class="label col-3">Name</div>
<div class="value col-9" v-text="info.server.host.name"></div>
</div>
<div class="row" v-if="info.server.host.port">
<div class="label col-3">Port</div>
<div class="value col-9" v-text="info.server.host.port"></div>
</div>
<div class="row" v-if="info.server.host.os && info.server.host.os.length">
<div class="label col-3">OS</div>
<div class="value col-9" v-text="info.server.host.os"></div>
</div>
<div class="row" v-if="info.server.host.arch && info.server.host.arch.length">
<div class="label col-3">Architecture</div>
<div class="value col-9" v-text="info.server.host.arch"></div>
</div>
<div class="row">
<div class="label col-3">Server name</div>
<div class="value col-9" v-text="info.server.snapserver.name"></div>
</div>
<div class="row">
<div class="label col-3">Server version</div>
<div class="value col-9" v-text="info.server.snapserver.version"></div>
</div>
<div class="row">
<div class="label col-3">Protocol version</div>
<div class="value col-9" v-text="info.server.snapserver.protocolVersion"></div>
</div>
<div class="row">
<div class="label col-3">Control protocol version</div>
<div class="value col-9" v-text="info.server.snapserver.controlProtocolVersion"></div>
</div>
</div>
</music-snapcast-host-info>
</modal> </modal>
<modal id="music-snapcast-group-info" title="Group info" v-if="modal.group.visible" v-model="modal.group.visible" ref="modalGroup"> <modal id="music-snapcast-group-info" title="Group info" v-if="modal.group.visible" v-model="modal.group.visible" ref="modalGroup">
<music-snapcast-group-info :info="modal.group.info" inline-template> {% include 'plugins/music.snapcast/modals/group.html' %}
<p>IT WORKED!</p>
</music-snapcast-group-info>
</modal> </modal>
<modal id="music-snapcast-client-info" title="Client info" v-if="modal.client.visible" v-model="modal.client.visible" ref="modalClient"> <modal id="music-snapcast-client-info" title="Client info" v-if="modal.client.visible" v-model="modal.client.visible" ref="modalClient">
<music-snapcast-client-info :info="modal.client.info" inline-template> {% include 'plugins/music.snapcast/modals/client.html' %}
<p>IT WORKED!</p>
</music-snapcast-client-info>
</modal> </modal>
<music-snapcast-host <music-snapcast-host

View file

@ -0,0 +1,76 @@
<music-snapcast-client-info :info="modal.client.info" inline-template>
<div class="info">
<div class="row">
<div class="label col-3">ID</div>
<div class="value col-9" v-text="info.id"></div>
</div>
<div class="row" v-if="info.config.name.length || info.host.name">
<div class="label col-3">Name</div>
<div class="value col-9" v-text="info.config.name.length || info.host.name"></div>
</div>
<div class="row">
<div class="label col-3">Connected</div>
<div class="value col-9" v-text="info.connected"></div>
</div>
<div class="row">
<div class="label col-3">Volume</div>
<div class="value col-9">{% raw %}{{ info.config.volume.percent }}%{% endraw %}</div>
</div>
<div class="row">
<div class="label col-3">Muted</div>
<div class="value col-9" v-text="info.config.volume.muted"></div>
</div>
<div class="row">
<div class="label col-3">Latency</div>
<div class="value col-9" v-text="info.config.latency"></div>
</div>
<div class="row" v-if="info.host.ip && info.host.ip.length">
<div class="label col-3">IP Address</div>
<div class="value col-9" v-text="info.host.ip"></div>
</div>
<div class="row" v-if="info.host.mac && info.host.mac.length">
<div class="label col-3">MAC Address</div>
<div class="value col-9" v-text="info.host.mac"></div>
</div>
<div class="row" v-if="info.host.os && info.host.os.length">
<div class="label col-3">OS</div>
<div class="value col-9" v-text="info.host.os"></div>
</div>
<div class="row" v-if="info.host.arch && info.host.arch.length">
<div class="label col-3">Architecture</div>
<div class="value col-9" v-text="info.host.arch"></div>
</div>
<div class="row">
<div class="label col-3">Client name</div>
<div class="value col-9" v-text="info.snapclient.name"></div>
</div>
<div class="row">
<div class="label col-3">Client version</div>
<div class="value col-9" v-text="info.snapclient.version"></div>
</div>
<div class="row">
<div class="label col-3">Protocol version</div>
<div class="value col-9" v-text="info.snapclient.protocolVersion"></div>
</div>
<div class="row buttons">
<button type="button" class="delete" :disabled="loading" @click="deleteClient">
<i class="fa fa-trash"></i>
&nbsp; Delete client
</button>
</div>
</div>
</music-snapcast-client-info>

View file

@ -0,0 +1,56 @@
<music-snapcast-group-info :info="modal.group.info" inline-template>
<div class="info">
<div class="section clients" v-if="Object.keys(info.clients).length > 0">
<div class="title">Clients</div>
<div class="row" ref="groupClients" v-for="client in info.clients">
<input type="checkbox"
@input="onClientUpdate"
class="client"
:id="'snapcast-client-' + client.id"
:value="client.id"
:disabled="loading"
:checked="client.id in info.group.clients">
<label :for="'snapcast-client-' + client.id" v-text="client.host.name"></label>
</div>
</div>
<div class="section streams" v-if="info.group.stream_id">
<div class="title">Stream</div>
<div class="row">
<div class="label col-3">ID</div>
<div class="value col-9">
<select @input="onStreamUpdate" :disabled="loading" ref="streamSelect">
<option
@input="onStreamUpdate"
v-for="stream in info.streams"
v-text="info.streams[info.group.stream_id].id"
:name="stream.id"
:selected="stream.id === info.group.stream_id">
</option>
</select>
</div>
</div>
<div class="row">
<div class="label col-3">Status</div>
<div class="value col-9" v-text="info.streams[info.group.stream_id].status"></div>
</div>
<div class="row" v-if="info.streams[info.group.stream_id].uri.host">
<div class="label col-3">Host</div>
<div class="value col-9" v-text="info.streams[info.group.stream_id].uri.host"></div>
</div>
<div class="row">
<div class="label col-3">Path</div>
<div class="value col-9" v-text="info.streams[info.group.stream_id].uri.path"></div>
</div>
<div class="row">
<div class="label col-3">URI</div>
<div class="value col-9" v-text="info.streams[info.group.stream_id].uri.raw"></div>
</div>
</div>
</div>
</music-snapcast-group-info>

View file

@ -0,0 +1,54 @@
<music-snapcast-host-info v-if="modal.host.visible" :info="modal.host.info" inline-template>
<div class="info">
<div class="row" v-if="info.server.host.ip && modal.host.info.server.host.ip.length">
<div class="label col-3">IP Address</div>
<div class="value col-9" v-text="modal.host.info.server.host.ip"></div>
</div>
<div class="row" v-if="info.server.host.mac && info.server.host.mac.length">
<div class="label col-3">MAC Address</div>
<div class="value col-9" v-text="info.server.host.mac"></div>
</div>
<div class="row" v-if="info.server.host.name && info.server.host.name.length">
<div class="label col-3">Name</div>
<div class="value col-9" v-text="info.server.host.name"></div>
</div>
<div class="row" v-if="info.server.host.port">
<div class="label col-3">Port</div>
<div class="value col-9" v-text="info.server.host.port"></div>
</div>
<div class="row" v-if="info.server.host.os && info.server.host.os.length">
<div class="label col-3">OS</div>
<div class="value col-9" v-text="info.server.host.os"></div>
</div>
<div class="row" v-if="info.server.host.arch && info.server.host.arch.length">
<div class="label col-3">Architecture</div>
<div class="value col-9" v-text="info.server.host.arch"></div>
</div>
<div class="row">
<div class="label col-3">Server name</div>
<div class="value col-9" v-text="info.server.snapserver.name"></div>
</div>
<div class="row">
<div class="label col-3">Server version</div>
<div class="value col-9" v-text="info.server.snapserver.version"></div>
</div>
<div class="row">
<div class="label col-3">Protocol version</div>
<div class="value col-9" v-text="info.server.snapserver.protocolVersion"></div>
</div>
<div class="row">
<div class="label col-3">Control protocol version</div>
<div class="value col-9" v-text="info.server.snapserver.controlProtocolVersion"></div>
</div>
</div>
</music-snapcast-host-info>

View file

@ -34,7 +34,7 @@ class MusicSnapcastBackend(Backend):
_DEFAULT_POLL_SECONDS = 10 # Poll servers each 10 seconds _DEFAULT_POLL_SECONDS = 10 # Poll servers each 10 seconds
_SOCKET_EOL = '\r\n'.encode() _SOCKET_EOL = '\r\n'.encode()
def __init__(self, hosts=['localhost'], ports=[_DEFAULT_SNAPCAST_PORT], def __init__(self, hosts=None, ports=None,
poll_seconds=_DEFAULT_POLL_SECONDS, *args, **kwargs): poll_seconds=_DEFAULT_POLL_SECONDS, *args, **kwargs):
""" """
:param hosts: List of Snapcast server names or IPs to monitor (default: :param hosts: List of Snapcast server names or IPs to monitor (default:
@ -52,6 +52,11 @@ class MusicSnapcastBackend(Backend):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if ports is None:
ports = [self._DEFAULT_SNAPCAST_PORT]
if hosts is None:
hosts = ['localhost']
self.hosts = hosts[:] self.hosts = hosts[:]
self.ports = ports[:] self.ports = ports[:]
self.poll_seconds = poll_seconds self.poll_seconds = poll_seconds
@ -123,10 +128,10 @@ class MusicSnapcastBackend(Backend):
elif msg.get('method') == 'Group.OnStreamChanged': elif msg.get('method') == 'Group.OnStreamChanged':
group_id = msg.get('params', {}).get('id') group_id = msg.get('params', {}).get('id')
stream_id = msg.get('params', {}).get('stream_id') stream_id = msg.get('params', {}).get('stream_id')
evt = GroupStreamChangeEvent(host=host, group=group, stream=stream) evt = GroupStreamChangeEvent(host=host, group=group_id, stream=stream_id)
elif msg.get('method') == 'Stream.OnUpdate': elif msg.get('method') == 'Stream.OnUpdate':
stream_id = msg.get('params', {}).get('stream_id')
stream = msg.get('params', {}).get('stream') stream = msg.get('params', {}).get('stream')
stream_id = stream.get('id')
evt = StreamUpdateEvent(host=host, stream_id=stream_id, stream=stream) evt = StreamUpdateEvent(host=host, stream_id=stream_id, stream=stream)
elif msg.get('method') == 'Server.OnUpdate': elif msg.get('method') == 'Server.OnUpdate':
server = msg.get('params', {}).get('server') server = msg.get('params', {}).get('server')
@ -137,10 +142,9 @@ class MusicSnapcastBackend(Backend):
def _client(self, host, port): def _client(self, host, port):
def _thread(): def _thread():
set_thread_name('Snapcast-' + host) set_thread_name('Snapcast-' + host)
status = None
try: try:
status = self._status(host, port) self._status(host, port)
except Exception as e: except Exception as e:
self.logger.warning(('Exception while getting the status ' + self.logger.warning(('Exception while getting the status ' +
'of the Snapcast server {}:{}: {}'). 'of the Snapcast server {}:{}: {}').
@ -200,7 +204,7 @@ class MusicSnapcastBackend(Backend):
except Exception as e: except Exception as e:
self.logger.warning('Unable to connect to {}:{}: {}'.format( self.logger.warning('Unable to connect to {}:{}: {}'.format(
host, port, str(e))) host, port, str(e)))
self._socks[hosts] = None self._socks[host] = None
def run(self): def run(self):
super().run() super().run()