add view to controls stations

This commit is contained in:
bkfox 2016-07-29 18:44:39 +02:00
parent 5329e983a4
commit 9728acef81
8 changed files with 378 additions and 4 deletions

View File

@ -80,6 +80,10 @@ class Station(programs.Nameable):
sources.append(self.dealer) sources.append(self.dealer)
return sources return sources
@property
def all_sources(self):
return self.get_sources(dealer = True)
@property @property
def stream_sources(self): def stream_sources(self):
return self.get_sources(type = Source.Type.stream) return self.get_sources(type = Source.Type.stream)
@ -351,7 +355,6 @@ class Source(programs.Nameable):
self.controller.playlist = [ sound.path for sound in self.controller.playlist = [ sound.path for sound in
programs.Sound.objects.filter( programs.Sound.objects.filter(
type = programs.Sound.Type.archive, type = programs.Sound.Type.archive,
removed = False,
path__startswith = program.path path__startswith = program.path
) )
] ]

View File

@ -192,7 +192,6 @@ class Monitor:
diff = programs.Diffusion.objects.get_at(now).filter( diff = programs.Diffusion.objects.get_at(now).filter(
type = programs.Diffusion.Type.normal, type = programs.Diffusion.Type.normal,
sound__type = programs.Sound.Type.archive, sound__type = programs.Sound.Type.archive,
sound__removed = False,
**args **args
).distinct().order_by('start').first() ).distinct().order_by('start').first()
return (diff, diff and diff.playlist or []) return (diff, diff and diff.playlist or [])

View File

@ -61,6 +61,12 @@ class StationController(plugins.StationController):
except: except:
self.current_source = None self.current_source = None
def skip(self):
"""
Skip a given source. If no source, use master.
"""
self._send(self.station.id_, '.skip')
class SourceController(plugins.SourceController): class SourceController(plugins.SourceController):
rid = None rid = None
@ -86,7 +92,7 @@ class SourceController(plugins.SourceController):
""" """
Skip a given source. If no source, use master. Skip a given source. If no source, use master.
""" """
self._send(self.source.slug, '.skip') self._send(self.source.id_, '.skip')
def fetch(self): def fetch(self):
data = self._send(self.source.id_, '.get', parse = True) data = self._send(self.source.id_, '.get', parse = True)

View File

@ -154,7 +154,8 @@ class StationController:
""" """
Skip the current sound on the station Skip the current sound on the station
""" """
pass if self.current_source:
self.current_source.controller.skip()
class SourceController: class SourceController:

View File

@ -0,0 +1,125 @@
{% comment %}
TODO: update doc
Base configuration file to configure a station on liquidsoap.
# Interactive elements:
An interactive element is accessible to the people, in order to:
- get metadata
- skip the current sound
- enable/disable it
# Element of the context
We use theses elements from the template's context:
- controller: controller describing the station itself
- settings: global settings
# Overwrite the template
It is possible to overwrite the template, there are blocks at different
position in order to do it. Keep in mind that you might want to avoid to
put station specific configuration in the template itself.
{% endcomment %}
{% comment %}
An interactive source is a source that:
- is skippable through the given id on external interfaces
- can be disabled
- store metadata
{% endcomment %}
def interactive_source (id, s, ~active=true, ~disable_switch=false) =
s = store_metadata(id=id, size=1, s)
add_skip_command(s)
if disable_switch then
s
else
at(interactive.bool('#{id}_active', active), s)
end
end
{% comment %}
a stream is a source that:
- is a playlist on random mode (playlist object accessible at {id}_playlist
- is interactive
{% endcomment %}
def stream (id, file) =
s = playlist(id = '#{id}_playlist', mode = "random", file)
interactive_source(id, s)
end
{% block post_funcs %}
{% endblock %}
{# config #}
set("server.socket", true)
set("server.socket.path", "{{ station.controller.socket_path }}")
set("log.file.path", "{{ station.controller.station.path }}/liquidsoap.log")
{% for key, value in settings.AIRCOX_LIQUIDSOAP_SET.items %}
set("{{ key|safe }}", {{ value|safe }})
{% endfor %}
{% block post_config %}
{% endblock %}
{# station #}
{{ station.id_ }} = interactive_source (
"{{ station.id_ }}",
fallback(track_sensitive = false, [
{% for source in station.file_sources %}
{% with controller=source.controller %}
interactive_source(
'{{ source.id_ }}', single("{{ source.url }}"),
active=false
),
{% endwith %}
{% endfor %}
{% with source=station.dealer %}
{% with controller=source.controller %}
interactive_source('{{ source.id_ }}',
playlist.once(reload_mode='watch', "{{ controller.path }}"),
active=false
),
{% endwith %}
{% endwith %}
rotate([
{% for source in station.stream_sources %}
{% with controller=source.controller stream=source.controller.stream %}
{% if stream.delay %}
delay({{ stream.delay }}.,
stream("{{ source.id_ }}", "{{ controller.path }}")),
{% elif stream.begin and stream.end %}
at({ {{stream.begin}}-{{stream.end}} },
stream("{{ source.id_ }}", "{{ controller.path }}")),
{% elif not stream %}
stream("{{ source.id_ }}", "{{ controller.path }}"),
{% endif %}
{% endwith %}
{% endfor %}
]),
{% for source in station.fallback_sources %}
{% with controller=source.controller %}
single("{{ source.value }}"),
{% endwith %}
{% endfor %}
blank(id="blank_fallback", duration=0.1),
]),
disable_switch=true
)
{% block outputs %}
{% for output in station.outputs %}
output.{{ output.get_type_display }}(
{{ station.id_ }},
{% if controller.settings %},
{{ output.settings }}
{% endif %}
)
{% endfor %}
{% endblock %}
{% block post_output %}
{% endblock %}

View File

@ -0,0 +1,121 @@
{% load i18n %}
<style>
section.station {
padding: 0.4em;
font-size: 0.9em;
}
section.station header {
margin: 0.4em 0em;
}
section.station header > * {
margin: 0em 0.2em;
}
section.station h1 {
display: inline;
margin: 0px;
font-size: 1.4em;
}
section.station button {
float: right;
}
section.station .sources {
border: 1px grey solid;
}
section.station .source {
margin: 0.2em 0em;
}
section.station .name {
display: inline-block;
width: 10em;
}
section.station .file {
color: #007EDF;
}
section.station .source.current:before {
content: '▶';
color: red;
margin: 0em 1em;
}
</style>
<script>
// HERE
var Monitor = {
get_token: function () {
return document.cookie.replace(/.*csrftoken=([^;]+)(;.*|$)/, '$1');
},
post: function(station, action) {
params = 'station=' + station + '&&action=' + action;
req = new XMLHttpRequest()
req.open('POST', '{% url 'controllers.monitor' %}', false);
req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
req.setRequestHeader("Content-length", params.length);
req.setRequestHeader("Connection", "close");
req.setRequestHeader("X-CSRFToken", this.get_token());
req.send(params);
this.update();
},
skip: function(station) {
this.post(station, 'skip');
},
update: function(timeout) {
req = new XMLHttpRequest()
req.open('GET', '{% url 'controllers.monitor' %}', true);
req.onreadystatechange = function() {
if(req.readyState != 4 || (req.status != 200 && req.status != 0))
return;
var doc = document.implementation.createHTMLDocument('xhr')
.documentElement;
doc.innerHTML = req.responseText;
document.getElementById('stations').innerHTML =
doc.querySelector('#stations').innerHTML;
if(timeout)
window.setTimeout(function() { Monitor.update(timeout);}, 5000);
};
req.send();
},
}
Monitor.update(1000);
</script>
<div id='stations'>
{% for station in stations %}
<section class="station">
<header>
<h1>{{ station.name }}</h1>
<button onclick="Monitor.skip('{{ station.name }}');">{% trans "skip" %}</button>
<button onclick="Monitor.update();">{% trans "update" %}</button>
</header>
<div class="sources">
{% for source in station.all_sources %}
{% if source.controller.current_sound %}
<div class="source{% if source == station.controller.current_source %} current{% endif %}">
<span class="name">{{ source.name }}</span>
<span class="file">{{ source.controller.current_sound }}</span>
</div>
{% endif %}
{% endfor %}
</div>
</section>
{% endfor %}
</div>

9
controllers/urls.py Normal file
View File

@ -0,0 +1,9 @@
from django.conf.urls import include, url
import aircox.controllers.views as views
urls = [
url(r'^monitor', views.Monitor.as_view(), name='controllers.monitor'),
url(r'^on_air', views.on_air, name='controllers.on_air'),
]

110
controllers/views.py Normal file
View File

@ -0,0 +1,110 @@
import json
from django.views.generic.base import View, TemplateResponseMixin
from django.http import HttpResponse
from django.shortcuts import render
from django.utils.translation import ugettext as _, ugettext_lazy
from django.utils import timezone as tz
import aircox.controllers.models as models
class Stations:
stations = models.Station.objects.all()
update_timeout = None
fetch_timeout = None
def fetch(self):
if self.fetch_timeout and self.fetch_timeout > tz.now():
return
self.fetch_timeout = tz.now() + tz.timedelta(seconds = 5)
for station in self.stations:
station.prepare(fetch = True)
stations = Stations()
def on_air(request):
try:
import aircox.cms.models as cms
except:
cms = None
station = request.GET.get('station');
if station:
station = stations.stations.filter(name = station)
else:
station = stations.stations.first()
last = station.on_air(count = 1)
if not last:
return HttpResponse('')
last = last[0]
if type(last) == models.Log:
last = {
'type': 'track',
'artist': last.related.artist,
'title': last.related.title,
'date': last.date,
}
else:
try:
publication = None
if cms:
publication = \
cms.DiffusionPage.objects.filter(
diffusion = last.initial or last).first() or \
cms.ProgramPage.objects.filter(
program = last.program).first()
except:
pass
last = {
'type': 'diffusion',
'title': publication.title if publication else last.program.name,
'date': last.start,
'url': publication.specific.url if publication else None,
}
last['date'] = str(last['date'])
return HttpResponse(json.dumps(last))
class Monitor(View,TemplateResponseMixin):
template_name = 'aircox/controllers/monitor.html'
def get_context_data(self, **kwargs):
stations.fetch()
return { 'stations': stations.stations }
def get (self, request = None, **kwargs):
self.request = request
context = self.get_context_data(**kwargs)
return render(request, self.template_name, context)
def post (self, request = None, **kwargs):
if not 'action' in request.POST:
return HttpResponse('')
POST = request.POST
controller = POST.get('controller')
action = POST.get('action')
station = stations.stations.filter(name = POST.get('station')) \
.first()
if not station:
return HttpResponse('')
station.prepare(fetch=True)
if station and action == 'skip':
station.controller.skip()
return HttpResponse('')