Finalized new Snapcast webpanel plugin
This commit is contained in:
parent
95a9c22618
commit
91ef6f3ce2
12 changed files with 382 additions and 100 deletions
|
@ -1,5 +1,5 @@
|
|||
.modal-container {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
@ -70,11 +70,58 @@ $host-shadow: $default-shadow;
|
|||
|
||||
.modal {
|
||||
.info {
|
||||
.row {
|
||||
&:hover { background: $hover-bg; }
|
||||
.value {
|
||||
text-align: right;
|
||||
padding: 2rem;
|
||||
|
||||
.section {
|
||||
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')} {
|
||||
.music-snapcast-container {
|
||||
.modal {
|
||||
width: 65vw;
|
||||
width: 45vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,35 @@ Vue.component('music-snapcast-client', {
|
|||
});
|
||||
|
||||
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' }
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -22,6 +22,59 @@ Vue.component('music-snapcast-group', {
|
|||
});
|
||||
|
||||
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',
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@ Vue.component('music-snapcast-host', {
|
|||
});
|
||||
|
||||
Vue.component('music-snapcast-host-info', {
|
||||
props: ['info'],
|
||||
props: {
|
||||
info: { type: Object }
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ Vue.component('music-snapcast', {
|
|||
data: function() {
|
||||
return {
|
||||
hosts: {},
|
||||
ports: {},
|
||||
modal: {
|
||||
host: {
|
||||
visible: false,
|
||||
|
@ -24,17 +25,8 @@ Vue.component('music-snapcast', {
|
|||
},
|
||||
|
||||
methods: {
|
||||
refresh: async function() {
|
||||
let hosts = await request('music.snapcast.get_backend_hosts');
|
||||
let promises = Object.keys(hosts).map(
|
||||
(host) => request('music.snapcast.status', {host: host, port: hosts[host]})
|
||||
);
|
||||
|
||||
let statuses = await Promise.all(promises);
|
||||
this.hosts = {};
|
||||
|
||||
for (const status of statuses) {
|
||||
status.server.host.port = hosts[status.server.host.name];
|
||||
_parseServerStatus: function(status) {
|
||||
status.server.host.port = this.ports[status.server.host.name];
|
||||
var groups = {};
|
||||
|
||||
for (const group of status.groups) {
|
||||
|
@ -56,6 +48,20 @@ Vue.component('music-snapcast', {
|
|||
|
||||
status.streams = streams;
|
||||
Vue.set(this.hosts, status.server.host.name, status);
|
||||
},
|
||||
|
||||
refresh: async function() {
|
||||
let hosts = await request('music.snapcast.get_backend_hosts');
|
||||
let promises = Object.keys(hosts).map(
|
||||
(host) => request('music.snapcast.status', {host: host, port: hosts[host]})
|
||||
);
|
||||
|
||||
let statuses = await Promise.all(promises);
|
||||
this.hosts = {};
|
||||
|
||||
for (const status of statuses) {
|
||||
this.ports[status.server.host.name] = hosts[status.server.host.name];
|
||||
this._parseServerStatus(status);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -72,11 +78,11 @@ Vue.component('music-snapcast', {
|
|||
},
|
||||
|
||||
onServerUpdate: function(event) {
|
||||
this.refresh();
|
||||
this._parseServerStatus(event.server);
|
||||
},
|
||||
|
||||
onStreamUpdate: function(event) {
|
||||
this.streams[event.stream_id] = event.stream;
|
||||
this.hosts[event.host].streams[event.stream.id] = event.stream;
|
||||
},
|
||||
|
||||
onClientVolumeChange: function(event) {
|
||||
|
@ -103,10 +109,21 @@ Vue.component('music-snapcast', {
|
|||
this.modal[event.type].info = this.hosts[event.host];
|
||||
break;
|
||||
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;
|
||||
case '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;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="head">
|
||||
<div class="col-10 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 %}
|
||||
</div>
|
||||
<div class="col-2 switch pull-right">
|
||||
|
|
|
@ -3,71 +3,15 @@
|
|||
<script type="text/x-template" id="tmpl-music-snapcast">
|
||||
<div class="row music-snapcast-container">
|
||||
<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>
|
||||
<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>
|
||||
{% include 'plugins/music.snapcast/modals/host.html' %}
|
||||
</modal>
|
||||
|
||||
<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>
|
||||
<p>IT WORKED!</p>
|
||||
</music-snapcast-group-info>
|
||||
{% include 'plugins/music.snapcast/modals/group.html' %}
|
||||
</modal>
|
||||
|
||||
<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>
|
||||
<p>IT WORKED!</p>
|
||||
</music-snapcast-client-info>
|
||||
{% include 'plugins/music.snapcast/modals/client.html' %}
|
||||
</modal>
|
||||
|
||||
<music-snapcast-host
|
||||
|
|
|
@ -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>
|
||||
Delete client
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</music-snapcast-client-info>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -34,7 +34,7 @@ class MusicSnapcastBackend(Backend):
|
|||
_DEFAULT_POLL_SECONDS = 10 # Poll servers each 10 seconds
|
||||
_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):
|
||||
"""
|
||||
:param hosts: List of Snapcast server names or IPs to monitor (default:
|
||||
|
@ -52,6 +52,11 @@ class MusicSnapcastBackend(Backend):
|
|||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if ports is None:
|
||||
ports = [self._DEFAULT_SNAPCAST_PORT]
|
||||
if hosts is None:
|
||||
hosts = ['localhost']
|
||||
|
||||
self.hosts = hosts[:]
|
||||
self.ports = ports[:]
|
||||
self.poll_seconds = poll_seconds
|
||||
|
@ -123,10 +128,10 @@ class MusicSnapcastBackend(Backend):
|
|||
elif msg.get('method') == 'Group.OnStreamChanged':
|
||||
group_id = msg.get('params', {}).get('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':
|
||||
stream_id = msg.get('params', {}).get('stream_id')
|
||||
stream = msg.get('params', {}).get('stream')
|
||||
stream_id = stream.get('id')
|
||||
evt = StreamUpdateEvent(host=host, stream_id=stream_id, stream=stream)
|
||||
elif msg.get('method') == 'Server.OnUpdate':
|
||||
server = msg.get('params', {}).get('server')
|
||||
|
@ -137,10 +142,9 @@ class MusicSnapcastBackend(Backend):
|
|||
def _client(self, host, port):
|
||||
def _thread():
|
||||
set_thread_name('Snapcast-' + host)
|
||||
status = None
|
||||
|
||||
try:
|
||||
status = self._status(host, port)
|
||||
self._status(host, port)
|
||||
except Exception as e:
|
||||
self.logger.warning(('Exception while getting the status ' +
|
||||
'of the Snapcast server {}:{}: {}').
|
||||
|
@ -200,7 +204,7 @@ class MusicSnapcastBackend(Backend):
|
|||
except Exception as e:
|
||||
self.logger.warning('Unable to connect to {}:{}: {}'.format(
|
||||
host, port, str(e)))
|
||||
self._socks[hosts] = None
|
||||
self._socks[host] = None
|
||||
|
||||
def run(self):
|
||||
super().run()
|
||||
|
|
Loading…
Reference in a new issue